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

Post on 16-Jul-2015

596 views 2 download

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

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 projekttego 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 <- xs, x `mod` p /= 0]

• Później przeniesiona na 25 innych języków programowania

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 <- arbitrary[Int]) yield (2 * n)

val primeInts = Gen.choose(0, 1000).filter(isPrime(_))

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)

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 ScalaTestclass 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 stanowegoclass 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<String> words : someLists(strings())) {

char separator = ',';

String joined = Joiner.on(separator).join(words);

List<String> 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 << someLists(integers(), 100)

}

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 << Gen.these('').then(Gen.string).take(10000)

}

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ę!