...

4Developers 2015: Property-based testing w języku Scala - Paweł Grajewski

by proidea

on

Report

Category:

Software

Download: 0

Comment: 0

592

views

Comments

Description

Download 4Developers 2015: Property-based testing w języku Scala - Paweł Grajewski

Transcript

  • Property-based testing w języku Scala 2015.04.20 Paweł Grajewski @BMS_devs
  • – Edsger W. Dijkstra "Program testing can be used to show the presence of bugs, but never to show their absence!"
  • Dlaczego tak jest?
  • Co z tym zrobić?
  • 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]
  • Rozwiązanie?
  • Czy ktoś to robi?
  • Telekomunikacja • Lucent • seria przełączników PathStar • potencjalnie największy projekt
 tego typu w historii • Ericsson • stacje bazowe dla technologii LTE
  • Motoryzacja • Volvo • testy oprogramowania mikroprocesorów • Toyota • analiza oprogramowania Toyoty Camry w ramach śledztwa w sprawie tzw. sudden unintended acceleration • analiza prowadzona przez NASA (!)
  • 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
  • Maeslantkering • Nieuwe Waterweg k. Rotterdamu • 200 tys. linii kodu produkcyjnego • 250 tys. linii kodu testów, symulacji oraz oprogramowania pomocniczego
  • A w skali mikro?
  • QuickCheck • Pierwsza przystępna i najbardziej popularna implementacja • Zaimplementowana w języku Haskell: primes = sieve [2..] where sieve (p:xs) = p : sieve [x | x
  • Ale Haskell…?
  • ScalaCheck
  • 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)
  • Język opisu własności • Operator forAll • Przykład: forAll { (text: String) => md5(text).matches("^[0-9a-f]{32}$") }
  • 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)]
  • 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) => … }
  • Generowanie danych • Predefiniowane statyczne generatory: • Char: Gen.numChar, Gen.alphaLowerChar, Gen.alphaUpperChar, Gen.alphaChar, Gen.alphaNumChar • String: Gen.identifier, Gen.alphaStr, Gen.numStr • Number: Gen.posNum, Gen.negNum • UUID: Gen.uuid forAll (Gen.alphaStr) { (s: String) => … }
  • 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
  • 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
  • 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: "궯"
  • 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 } }
  • Uruchamianie testów • Integracja z frameworkami testującymi: • ScalaTest • specs2
  • 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)
 }
 }
 } }
  • 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
 }
 } }
  • 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 }} """ }
  • Tyle teorii o ScalaCheck
  • Kontrowersje
  • Prominentne projekty
  • 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)
  • 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
 }
  • 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
 }
  • Lepsze przykłady • Testowanie interfejsów: forAll { (company: Company) =>
 val output = exportCompany(company)
 
 (output.vatStatus == true) 
 
 (company.legalForm == SP_ZOO) ==>
 (output.representation == true) 
 }
  • Testowanie kodu stanowego class Counter { private var n = 0 def inc = n += 1 def dec = if(n > 10) n -= 2 else n -= 1 //bug! def get = n } scala> CounterSpecification.check ! Falsified after 37 passed tests. > COMMANDS: Inc, Inc, Inc, Inc, Inc, Inc, Inc, Inc, Inc, Inc, Inc, Dec, Get (orig arg: Inc, Dec, Inc, Dec, Inc, Inc, Get, Inc, Inc, Get, Inc, Inc, Inc, Dec, Inc, Inc, Inc, Get, Dec, Inc, Inc, Inc, Dec, Dec, Inc, Get, Dec, Dec, Get, Inc, Dec, Get, Get, Inc, Inc, Inc, Get)
  • Warto spróbować?
  • 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.”
  • Komu i jak to pomogło?
  • 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ń
  • 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 (!!!!)
  • 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
  • Języki inne niż Scala?
  • Java • junit-quickcheck [https://github.com/pholser/junit-quickcheck] @RunWith(Theories.class) public class SymmetricKeyCryptography { @Theory public void decryptReversesEncrypt( @ForAll String plaintext, @ForAll Key key) { String ciphertext = crypto.encrypt(plaintext, key); assertEquals(plaintext, crypto.decrypt(ciphertext, key)); } }
  • Java • Java QuickCheck [https://bitbucket.org/blob79/quickcheck] @Test public void joinAndSplitTest() { for (List words : someLists(strings())) { char separator = ','; String joined = Joiner.on(separator).join(words); List output = Splitter.on(separator).split(input); assertEquals(words, output); } }
  • 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
  • Groovy/Spock • spock-genesis [https://github.com/Bijnagte/spock-genesis] def 'test for reversing strings'() { expect: def reversed = string.reverse() reversed.reverse() == string where: string
  • Inne języki • JavaScript: QC.js, JSVerify • Clojure: ClojureCheck • Python: Factcheck, Hypothesis • Ruby: Rantly • Mały stopień skomplikowania… napisać samemu?
  • Przesłanie
  • 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.
  • – 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.”
  • Pytania?
  • Dziękuję!
Fly UP