Created
April 29, 2010 17:26
-
-
Save skanev/383920 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Автоматизирано (unit) тестване | |
12.04.2009 | |
== Disclaimer == | |
Днес няма да си говорим за acceptance testing, quality assurance или нещо, което се прави от "по-низшия" отдел във фирмата. Всичко тук е дело на програмиста. | |
== Митът == | |
Проектът идва с готово, подробно задание. Прави се дизайн. С него работата се разбива на малки задачи. Те се извършват последователно. За всяка от тях пишете кода и приключвате. Изискванията не се променят, нито се добавя нова функционалност. | |
== Митът v1.1 == | |
Щом съм написал един код, значи ми остава единствено да го разцъкам - няколко `print`-а, малко пробване в `main` метода/функцията и толкова. Така или иначе няма да се променя. А ако (не дай си боже) това се случи - аз съм го писал, знам го, няма как да допусна грешка. Най-много да го поразцъкам още малко. | |
== Тежката действителност == | |
* ((Заданията ***винаги*** се променят.)) | |
* ((Често се налага един код да се преработва.)) | |
* ((Писането на код е сложна задача - допускат се грешки.)) | |
* ((Програмистите (освен Мило) са хора - допускат грешки (освен Мило).)) | |
* ((Промяната на модул в единия край на системата като нищо може да счупи модул в другия край на системата. )) | |
== Традиционния подход ==l | |
{{{python | |
class Programmer(object): | |
# ... | |
def implement_a_change(self, project, change): | |
files = self.open_related_files(project, change) | |
while True: | |
self.attempt_change(change, files) | |
project.run() | |
result = self.click_around_and_test(project) | |
project.stop() | |
if result.successful(): break | |
self.commit_code(project, files) | |
self.hope_everything_went_ok() | |
}}} | |
== Идея == | |
— Добре де... хващам се, че постоянно правя едно и също нещо като робот. Понеже е досадно, лесно ще забравя нещо. Пък и само ми губи времето. Човешката цивилизация не реши ли тоя вид проблеми с някакви машини? Май се казваха компютри? | |
| |
— Защо просто не си напишеш програма, която да го прави вместо теб? | |
== На хартия (или проектор) == | |
* ((За всичко съмнително ще пишем ***сценарий***, който да "цъка".)) | |
* ((Всеки сценарий ще изпълнява кода и ще прави няколко ***твърдения*** за резултатите.)) | |
* ((Сценариите ще бъдат обединени в ***групи***.)) | |
* ((Пускате всички тестове с едно бутонче.)) | |
* ((Резултатът е "Всичко мина успешно" или "Твърдения X, Y и Z в сценарии A, B и C се оказаха неверни".)) | |
== Кодът, който ще тестваме ==ll | |
{{{python | |
class Interval(object): | |
def __init__(self, left, right): self.left, self.right = left, right | |
def __repr__(self): return "Interval({0}, {1})".format(self.left, self.right) | |
def __eq__(self, other): | |
return isinstance(other, Interval) and \ | |
(self.left, self.right) == (other.left, other.right) | |
def left_open(self): return self.left == None | |
def right_open(self): return self.right == None | |
def contains_number(self, number): | |
if self.left_open() and self.right_open(): return True | |
if self.left_open(): return number <= self.right | |
if self.right_open(): return self.left <= number | |
return self.left < number < self.right | |
def intersect(self, other): | |
extr = lambda a, b, func: func(a, b) if not None in (a, b) else a or b | |
return Interval( | |
extr(self.left, other.left, max), | |
extr(self.right, other.right, min)) | |
__and__ = intersect | |
}}} | |
== Идеята... ==l | |
{{{python | |
class IntervalTest: | |
def test_contains_number(self): | |
interval = Interval(None, 0) | |
твърдя_че("interval съдържа -3") | |
твърдя_че("interval съдържа 0") | |
твърдя_че("interval не съдържа 9") | |
твърдя_че("interval.left_open() е истина") | |
твърдя_че("interval.right_open() е лъжа") | |
def test_intersects(self): | |
твърдя_че("сечението на [0, 10] с [5, None] е [5, 10]") | |
твърдя_че("сечението на [None, 0] с [None, 42] е [None, 0]") | |
твърдя_че("сечението на [None, 20] с [-20, None] е [-20, 20]") | |
твърдя_че("сечението на [None, 0] с [-10, None] е [-10, 0]") | |
}}} | |
== ...реализацията... ==l | |
{{{python | |
class IntervalTest(unittest.TestCase): | |
def test_contains_number(self): | |
interval = Interval(None, 0) | |
self.assertTrue(interval.contains_number(-3)) | |
self.assertTrue(interval.contains_number(0)) | |
self.failIf(interval.contains_number(9)) | |
self.assertTrue(interval.left_open()) | |
self.failIf(interval.right_open()) | |
def test_intersects(self): | |
self.assertEquals( | |
Interval(5, 10), Interval(0, 10) & Interval(5, None)) | |
self.assertEquals( | |
Interval(None, 0), Interval(None, 42) & Interval(None, 0)) | |
self.assertEquals( | |
Interval(-20, 20), Interval(None, 20) & Interval(-20, None)) | |
self.assertEquals( | |
Interval(-10, 0), Interval(None, 0) & Interval(-10, None)) | |
if __name__ == "__main__": | |
unittest.main() | |
}}} | |
== ...и резултата == | |
{{{ | |
.F | |
====================================================================== | |
FAIL: test_intersects (__main__.IntervalTest) | |
---------------------------------------------------------------------- | |
Traceback (most recent call last): | |
File "<stdin>", line 52, in test_intersects | |
AssertionError: Interval(-10, 0) != Interval(-10, None) | |
---------------------------------------------------------------------- | |
Ran 2 tests in 0.001s | |
FAILED (failures=1) | |
}}} | |
== bulgarian: (english, python) ==l | |
{{{python | |
vocabulary = { | |
"група": ("test case", unittest.TestCase), | |
"сценарий": ("test method", | |
[_ for _ in dir(YourTestCase) if _.startswith("test")]), | |
"твърдение": ("assertion", | |
[_ for _ in dir(unittest.TestCase) if re.match("assert|fail", _)]) | |
} | |
}}} | |
***Важно.*** Не бъркайте ключовата дума `assert` с методите за твърдения в тестовете. Първото служи да прекратите програмата ако изпадне в невалидно състояние. Второто е част от библиотеката за тестове. | |
== Твърдения в unittest.TestCase ==l | |
Всички методи имат опционален последен аргумент `msg` - текстово съобщение, което ще се покаже ако теста пропадне. | |
* ((`self.assertTrue(expr)` - още `assert_` и `failUnless`)) | |
* ((`self.assertFalse(expr)` - още `failIf`)) | |
* ((`self.assertEqual(expected, actual)` - още `assertEquals` и `failUnlessEqual`)) | |
* ((`self.assertAlmostEqual(expected, actual, places=7)` - още `assertAlmostEquals` и `failUnlessAlmostEqual`)) | |
* ((`self.assertNotAlmostEqual(expected, actual, places=7)` - още `assertNotAlmostEquals` и `failIfAlmostEqual`)) | |
* ((`self.assertRaises(self, excClass, callable, *args, **kwargs)` - още `failUnlessRaises`)) | |
== Видове тестове == | |
* ((***Unit tests*** - проверяват дали дадено парче код/клас работи правилно в изолация)) | |
* ((***Integration tests*** - проверяват дали няколко модула си общуват правилно)) | |
* ((***Functional tests*** - проверяват дали крайната функционалност е както се очаква)) | |
== За какво ни помагат тестовете == | |
* ((Откриват грешки по-рано)) | |
* ((Позволяват ни уверено да правим промени в системата)) | |
* ((Дават сигурност на клиенти, шефове и програмисти)) | |
* ((Представляват пример как се работи с кода)) | |
* ((Помага разделянето на интерфейс от имплементация)) | |
* ((Служат като документация и спецификация)) | |
== За какво не служат тестовете == | |
* ((***Не доказват***, че приложението работи)) | |
* ((Не са Quality Assurance)) | |
* ((Не са benchmark)) | |
== Още речник == | |
* ((black-box тестове)) | |
* ((glass-box тестове)) | |
* ((fixture (`setUp` и `tearDown`))) | |
== Документация ==l | |
{{{python | |
class Foo: | |
""" | |
Sample Foo class | |
""" | |
def foo(self): | |
""" | |
Sample foo method | |
Returns: 2 | |
""" | |
return 2 | |
}}} | |
== Документацията като тестове ==l | |
{{{python | |
def add(a, b): | |
""" | |
Adds the two arguments. | |
>>> add(1, 3) | |
4 | |
>>> add(1, '') | |
Traceback (most recent call last): | |
... | |
TypeError: unsupported operand type(s) for +: 'int' and 'str' | |
""" | |
return a + b | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() | |
}}} | |
== Дизайн == | |
***Въпрос:*** какво е "дизайн" на едно приложение? | |
== Test-Driven Development == | |
<p><em>Test-Driven Development is not about testing.</em></p> | |
<p style="text-align: right">— Dan North</p> | |
== Test-Driven Development (2) == | |
# Добави тест | |
# Пусни всички тестове и виж, че новия се чупи | |
# Напиши код | |
# Пусни тестовете и виж че минават успешно | |
# Подобри кода (refactor) | |
# Повтаряй | |
== Демо == | |
лишън! | |
== Test-Driven Developmnt (3) == | |
* ((Подход за писане на код)) | |
* ((Дизайна е базиран върху обратна връзка, не гадаене)) | |
* ((Спестява излишен код -- пишете само каквото ви трябва)) | |
* ((Спестява излишна функционалност)) | |
* ((Продуктивност!)) | |
== Behaviour-Driven Development == | |
* Test-Driven Development by Example | |
* The RSpec Book | |
* xUnit Test Patterns | |
== Шепа съвети == | |
* ((***Пишете тестове за всичко, което може да се счупи.***)) | |
* ((Не тествайте елементарен код.)) | |
* ((Не използвайте произволни тестови данни.)) | |
* ((Успеха на тестовете не трябва да зависи от реда им.)) | |
* ((Тествайте гранични случаи!)) | |
* ((Не правете тестовете зависими един от друг.)) | |
== Още въпроси? == | |
* Страница на курса: http://fmi.py-bg.net/ | |
* Форуми на курса: http://fmi.py-bg.net/topics/ | |
* http://extremeprogramming.org/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
use strict; | |
use warnings; | |
use FileHandle; | |
use IPC::Open2; | |
sub produce { | |
$_ = $_[0]; | |
s/\n\r/\n/g; | |
s/\r/\n/g; | |
my @slides = split /^\s*(?===\s+.*?\s+==\w*$)/m; | |
print opening_slide(shift @slides)."\n\n"; | |
print format_slide($_)."\n\n" for (@slides); | |
print closing_slide(); | |
} | |
sub opening_slide { | |
$_ = shift; | |
my ($title, $date, $intro) = (/^(.*?)\n(.*?)\n(.*)$/s); | |
$intro = html_escape(trim($intro)); | |
$intro =~ s#\n#<br />\n#g; | |
$intro =~ s# # #g; | |
html_head($title, $date, $intro); | |
} | |
sub format_slide { | |
$_ = shift; | |
s/\{\{\{(\w+)?\n(.*?)\n\}\}\}/'{{{'.codenote($2, $1).'}}}'/smge; | |
s/"(.+?)"/„$1“/g; | |
s#^==\s+(.*?)\s+==(l+)?(.*)\z#'<div class="slide'.($2 ? " ".("long" x length($2)) : "")."\">\n <h1>$1</h1>$3\n</div>"#se; | |
s#\*\*\*(.*?)\*\*\*#<strong>$1</strong>#g; | |
s#`(.*?)`#<code>$1</code>#sg; | |
s#\[\[(.+?) (\S+?:\S+?)\]\]#<a href="$2">$1</a>#g; | |
s#(?<!")(http://\S+)#<a href="$1">$1</a>#g; | |
s#((^ \*.*?\n)+)# <ul>\n$1 </ul>\n#mg; | |
s#((^ \#.*?\n)+)# <ol>\n$1 </ol>\n#mg; | |
s/^ [\#*]\s*(.*)$/ <li>$1<\/li>/mg; | |
s#<li>\(\((.*?)\)\)</li>#<li class="incremental">$1</li>#g; | |
s#((^[^ <{\n].+?\n)+)#' <p>'.trim($1)."</p>\n"#mge; | |
s#<p>\(\((.*?)\)\)</p>#<p class="incremental">$1</p>#mg; | |
s/\n+/\n/g; | |
s/--/—/g; | |
s/\.\.\./…/g; | |
s/\{\{\{(\d+)\}\}\}/format_codenote($1)/ge; | |
$_; | |
} | |
# === CODE NOTES ========================================================== | |
our @codenotes; | |
sub codenote { | |
push @codenotes, {code => $_[0], format => $_[1]}; | |
$#codenotes; | |
} | |
sub format_codenote { | |
my ($note, $formats) = $codenotes[shift]; | |
$formats = { | |
bare => sub { $_[0] }, | |
code => sub { " <pre><code>".html_escape($_[0])."</code></pre>" }, | |
python => sub { " <pre class=\"prettyprint\">\n$_[0]\n</pre>" }, | |
with_output => sub { join "\n", $formats->{python}->($_[0]), $formats->{code}->(python_output($_[0])) }, | |
}; | |
no warnings; | |
($formats->{$note->{format}} || $formats->{python})->($note->{code}); | |
} | |
sub python_output { | |
my ($code, $result) = (shift); | |
open2(*Reader, *Writer, "python3.0"); | |
print Writer $code."\n"; | |
close Writer; | |
$result .= $_ while (<Reader>); | |
close Reader; | |
chomp $result; | |
$result; | |
} | |
# === TEXT UTILS ====================================================================== | |
sub trim { | |
$_ = shift; | |
s/^\s+//; | |
s/\s+$//; | |
$_; | |
} | |
sub html_escape { | |
$_ = shift; | |
s/&/&/g; | |
s/>/>/g; | |
s/</</g; | |
$_; | |
} | |
sub html_head { | |
my ($title, $date, $intro) = @_; | |
<<"HEAD" | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<title>Програмиране с Python: $title</title> | |
<!-- metadata --> | |
<meta name="generator" content="S5" /> | |
<meta name="version" content="S5 1.1" /> | |
<meta name="presdate" content="16.04.2008" /> | |
<meta name="author" content="Стефан Кънев, Николай Бачийски, Точо Точев и Димитър Димитров" /> | |
<!-- configuration parameters --> | |
<meta name="defaultView" content="slideshow" /> | |
<meta name="controlVis" content="hidden" /> | |
<!-- style sheet links --> | |
<link rel="stylesheet" href="ui/python/slides.css" type="text/css" media="projection" id="slideProj" /> | |
<link rel="stylesheet" href="ui/python/outline.css" type="text/css" media="screen" id="outlineStyle" /> | |
<link rel="stylesheet" href="ui/python/prettify.css" type="text/css" media="screen" id="prettyifyStyle" /> | |
<link rel="stylesheet" href="ui/python/print.css" type="text/css" media="print" id="slidePrint" /> | |
<link rel="stylesheet" href="ui/python/opera.css" type="text/css" media="projection" id="operaFix" /> | |
<!-- S5 JS --> | |
<script src="ui/python/slides.js" type="text/javascript"></script> | |
<script src="ui/python/prettify.js" type="text/javascript"></script> | |
</head> | |
<body> | |
<div class="layout"> | |
<div id="controls"><!-- DO NOT EDIT --></div> | |
<div id="currentSlide"><!-- DO NOT EDIT --></div> | |
<div id="header"></div> | |
<div id="footer"> | |
<h1>„$title“, част от курса <a href="http://fmi.py-bg.net/">Програмиране с Python</a></h1> | |
<h1>Текстът на тази презентация се разпространява под <a href="http://creativecommons.org/licenses/by/3.0/deed.bg">Creative Commons Attribution</a></h1> | |
</div> | |
</div> | |
<div class="presentation"> | |
<div class="slide"> | |
<h1>$title</h1> | |
<h3>„ Програмиране с Python“, <acronym title="Факултет по Математика и Информатика при Софийски Университет">ФМИ</acronym></h3> | |
<h4>$intro</h4> | |
<h4>$date</h4> | |
</div> | |
HEAD | |
; | |
} | |
sub closing_slide { | |
<<TAIL | |
</div> | |
<script type="text/javascript" language="JavaScript">prettyPrint()</script> | |
</body> | |
</html> | |
TAIL | |
; | |
} | |
produce join "", <>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment