Paweł Grajewski
Language: Polish
Techniki Test-driven development (TDD) oraz Behavior-driven development (BDD) są dziś powszechnie stosowaną metodą poprawy jakości wytwarzanego oprogramowania. Obie zakładają w swej konstrukcji budowę zestawu przypadków testowych, ale stworzenie poprawnego i kompletnego zestawu takich przypadków jest nie lada sztuką. Często nie jesteśmy w stanie przewidzieć wszystkich sytuacji brzegowych, w skutek czego nie możemy być pewni na ile w rzeczywistości poprawny jest kod naszej aplikacji
Z pomocą może przyjść nam technika property-based testing, która w miejsce testowania poprawności działania systemu dla skończonego zbioru przypadków testowych wprowadza koncepcję “badania jego właściwości”. Idea ta zrodziła się już wiele lat temu, a dzisiaj znowu wraca do łask wraz ze wzrostem popularności funkcyjnych języków programowania. Zaufało jej już wiele dużych, złożonych projektów m.in. projekt kompilatora języka Scala oraz framework Akka. Prawidłowo zastosowana jest w stanie zapewnić wymierne korzyści - zwiększyć pokrycie kodu testami, ale przede wszystkim uchronić nas przed wieloma typowymi niedopatrzeniami.
Prezentacja przybliży koncepcję property-based testing oraz zademonstruje tę technikę na przykładach.
5. Rozwiązanie?
• 1991: Spin (Promela)
• 1999: QuickCheck (Haskell)
• Automatyczne generowanie przypadków testowych
• “The programmer provides a specification of the
program, in the form of properties which functions
should satisfy, and QuickCheck then tests that the
properties hold in a large number of randomly
generated cases.” [source: QuickCheck manual]
8. Telekomunikacja
• Lucent
• seria przełączników PathStar
• potencjalnie największy projekt
tego typu w historii
• Ericsson
• stacje bazowe dla technologii LTE
9. Motoryzacja
• Volvo
• testy oprogramowania mikroprocesorów
• Toyota
• analiza oprogramowania Toyoty Camry w ramach
śledztwa w sprawie tzw. sudden unintended
acceleration
• analiza prowadzona przez NASA (!)
10. Misje kosmiczne
• NASA
• Mars Science Laboratory
• Mars Exploration Rover
• Deep Space 1, Cassini, Deep Impact
• algorytm hand-off pomiędzy CPU
• algorytm sterowania silnikami
• weryfikacja poprawności działania pamięci flash
11. Maeslantkering
• Nieuwe Waterweg k. Rotterdamu
• 200 tys. linii kodu produkcyjnego
• 250 tys. linii kodu testów, symulacji oraz oprogramowania
pomocniczego
13. QuickCheck
• Pierwsza przystępna i najbardziej popularna implementacja
• Zaimplementowana w języku Haskell:
primes = sieve [2..]
where sieve (p:xs) =
p : sieve [x | x <- xs, x `mod` p /= 0]
• Później przeniesiona na 25 innych języków programowania
16. ScalaCheck
• Framework napisany w języku Scala
• Umożliwiający testowanie kodu w językach Scala oraz Java
• Zapewnia:
• Język opisu własności, które powinien spełniać kod
• Mechanizm generowania danych wejściowych
• Mechanizm do uruchamiania testów oraz integrację
z frameworkami testującymi (specs2 i ScalaTest)
17. Język opisu własności
• Operator forAll
• Przykład:
forAll { (text: String) =>
md5(text).matches("^[0-9a-f]{32}$")
}
18. Generowanie danych
• Wsparcie “z pudełka” dla
generowania wartości typu:
• Boolean
• Byte, Short, Int,
Long, Float, Double
• BigInt, BigDecimal
• String, Char
• Number
• Date
• Throwable,
Exception,
Error
• Option[…],
Either[…, …]
• (…, …), (…, …, …), …
• Kolekcje np. List[String]
• Wielokrotne zagnieżdżenie np.
Set[(Set[String], Date)]
19. Generowanie danych
• Przydatne metody do definiowania własnych generatorów:
val colors = Gen.oneOf(“red”, “green”, “blue”)
val smallInts = Gen.choose(-1000, 1000)
val listsOfThreeNumbers = Gen.sequence(List(
Gen.choose(-10,-1),
Gen.const(0),
Gen.choose(1,10)
))
val vowels = Gen.frequency((3, 'A'), (4, ‘E'), (2, ‘I’),
(3, 'O'), (1, 'U'), (1, 'Y'))
forAll (smallInts) { (n: Int) => … }
21. Generowanie danych
• Trait Gen definiuje m.in. map, flatMap, filter, withFilter
• Możliwość wykorzystania w for-comprehension
• Łatwość transformacji generatorów w inne generatory
val fixedLengthStrings = (n: Int) =>
Gen.listOfN(n, Gen.alphaChar).map(_.mkString)
val evenInts = for (n <- arbitrary[Int]) yield (2 * n)
val primeInts = Gen.choose(0, 1000).filter(isPrime(_))
22. Generowanie danych
• For-comprehension czyni prostym generowanie całych obiektów danych
val nipNoGenerator = Gen.oneOf("8441900530", "1131946830")
val legalFormGenerator = Gen.oneOf(LegalForm.values.toSeq)
val companyGenerator = for {
name <- arbitrary[String]
nipNo <- nipNoGenerator
legalForm <- legalFormGenerator
} yield Company(name, nipNo, legalForm)
23. Uruchamianie testów
• Najprostszy sposób uruchomienia:
forAll { s: String =>
s.isEmpty
}.check ewentualnie: .check(100000)
• Wynik działania:
! Falsified after 1 passed tests.
> ARG_0: "궯"
24. Uruchamianie testów
• Suite’y testowe opisane bezpośrednio z wykorzystaniem ScalaCheck:
object ExampleInScalaCheck extends Properties("String") {
property("should be reversible") = forAll { s: String =>
s.reverse.reverse == s
}
property("should not be empty when it's length is greater
than zero") = forAll { s: String =>
(s.length > 0) ==> !s.isEmpty
}
}
26. Przykład w ScalaTest
class ExampleScalaTest extends WordSpec with PropertyChecks {
"String" should {
"be reversible" in {
forAll { s: String =>
assert(s.reverse.reverse == s)
}
}
"not be empty when it's length is greater than zero" in {
forAll { s: String =>
whenever(s.length > 0) {
assert(!s.isEmpty)
}
}
}
}
27. Przykład w specs2 (1/2)
class ExampleSpecs2 extends Specification with ScalaCheck {
"String" should {
"be reversible" in {
prop { s: String =>
s.reverse.reverse == s
}
}
"not be empty when it's length is greater than zero" in {
prop { s: String =>
(s.length > 0) ==> !s.isEmpty
}
}
}
28. Przykład w specs2 (2/2)
class ExampleSpecs2 extends Specification with ScalaCheck {
def is = s2”""
String should be reversible
${prop { (s: String) => s.reverse.reverse must_== s }}
String should not be empty when it's length is greater than zero
${prop { (s: String) => (s.length > 0) ==> !s.isEmpty }}
"""
}
32. Nietypowe przykłady
• Zbyt infantylne
• s.reverse().reverse() == s
• a+b == c
• Nadmienie teoretyczne (np. algebra, działania na
zbiorach, dowody przez indukcję, monoidy itp.)
• Rzeczywiście wymagające napisania logiki
biznesowej dwa razy (np. walidacja numeru NIP)
33. Lepsze przykłady
• Testowanie logiki biznesowej, która ze swojej natury jest symetryczna:
• serializacja/deserializacja
• szyfrowanie/odszyfrowywanie
• import/eksport
• …
forAll { (input: String, key: Array[Byte]) =>
val encrypted: String = encrypt(input, key)
val decrypted: String = decrypt(encrypted, key)
decrypted == input
}
34. Lepsze przykłady
• Testowanie logiki biznesowej, której wynik działania
powinien zachowywać określone właściwości:
forAll { (amount: BigDecimal, rate: BigDecimal,
numberOfMonths: Integer) =>
val schedule = paymentSchedule(amount = amount,
interestRate = rate,
numberOfMonths = numberOfMonths)
schedule.map(_.principalPayment).sum == amount
}
38. Specyfikacja wymagań
• Testy jednostkowe służące jako specyfikacja
• Odejście od przykładów na rzecz własności
• “(…) an approach to specification using properties
instead of tests with "magic" data is an alternative
which I think is often shorter and less ambiguous.”
40. Google LevelDB
• Google LevelDB: sortowany key-value store [http://leveldb.org]
• Joe Norton @ Lambda Jam, Chicago 2013
• Opisali model stanów z wykorzystaniem narzędzia QuickCheck
• Po kilku minutach od uruchomienia, QuickCheck znalazł sekwencję
poleceń prowadzącą do błędu (ciąg 17 zapytań do bazy danych)
• Kilka tygodni oczekiwania na poprawkę…
[https://code.google.com/p/leveldb/issues/detail?id=44]
• Po dalszych kilku minutach, QuickCheck znalazł kolejną sekwencję (!),
tym razem składającą się z 31 poleceń
41. Google LevelDB
• 1. open new database
• 2. put key1 and val1
• 3. close database
• 4. open database
• 5. delete key2
• 6. delete key1
• 7. close database
• 8. open database
• 9. delete key2
• 10. close database
• 11. open database
• 12. put key3 and val1
• 13. close database
• 14. open database
• 15. close database
• 16. open database
• 17. seek first
• oczekiwana wartość: key1
• otrzymana wartość: key3 (!!!!)
42. pflua
• pflua: filtr pakietów napisany w języku LUA [https://github.com/Igalia/pflua]
• Katerina Barone-Adesi @ FOSDEM, Belgium 2015
• Dwie osoby w jedno popołudnie napisały własne narzędzie, które testowało
tylko jedną własność:
• Input -> IR -> optimize(IR) -> compile -> run()
• Input -> IR -> compile -> run()
• Po uruchomieniu narzędzie wykryło 7 błędów w już gotowym, działającym,
przetestowanym i w miarę dojrzałym projekcie!
• Błędy bardzo trudne do wykrycia przy pomocy tradycyjnych technik testowania
45. Java
• Java QuickCheck [https://bitbucket.org/blob79/quickcheck]
@Test public void joinAndSplitTest() {
for (List<String> words : someLists(strings())) {
char separator = ',';
String joined = Joiner.on(separator).join(words);
List<String> output = Splitter.on(separator).split(input);
assertEquals(words, output);
}
}
46. Groovy/Spock
• z wykorzystaniem generatorów z Java QuickCheck
def 'sum of non-negative numbers should not be negative'() {
expect:
list.findAll {
it >= 0 }.sum() >= 0
where:
list << someLists(integers(), 100)
}
50. Property-based testing
• Kolejne narzędzie, jakie mamy do dyspozycji.
• W niektórych przypadkach faktycznie trudno jest je zastosować.
• W wielu miejscach jego wprowadzenie jest trywialne, a
potencjalne zyski bardzo duże.
• Możliwe do zaimplementowania w każdym języku
programowania.
• Niektórzy wprowadzając go do swoich projektów osiągali
spektakularne rezultaty
• Testy mogą przyjąć postać specyfikacji.
51. – Edsger W. Dijkstra
“If you want more effective programmers, you
will discover that they should not waste their
time debugging, they should not introduce the
bugs to start with.”