63
WHAT IS PURE FUNCTIONAL PROGRAMMING, AND HOW IT CAN IMPROVE OUR APPLICATION TESTING? Voxxed Days Zürich 2016-03-03 Voxxed Days Zürich 2016-03-03 1

What is Pure Functional Programming, and how it can improve our application testing?

Embed Size (px)

Citation preview

WHAT IS PURE FUNCTIONAL PROGRAMMING, AND HOW IT

CAN IMPROVE OUR APPLICATION TESTING?

Voxxed Days Zürich 2016-03-03

Voxxed Days Zürich 2016-03-03 1

About me

Luca Molteni

@volothamp

BitGold (h+ps://bitgold.com)

Voxxed Days Zürich 2016-03-03 2

Pure Func)onal Programming is fun

Voxxed Days Zürich 2016-03-03 3

Tes$ng is also fun

Voxxed Days Zürich 2016-03-03 4

Code quality

Voxxed Days Zürich 2016-03-03 5

Table of Content• What is Pure Func0onal Programming?

• Tes0ng

• Type Systems

• Conclusions

• Q & A

Voxxed Days Zürich 2016-03-03 6

What is Func,onal Programming?• Func&ons are first-class, that is, func&ons are values which can

be used in exactly the same ways as any other sort of value.

• FP languages are focused around evalua&ng expressions rather than execu&ng instruc&ons.

Voxxed Days Zürich 2016-03-03 7

What is "Pure Func0onal Programming?"

Voxxed Days Zürich 2016-03-03 8

What is Referen-al Transparency?

“An expression is said to be referen1ally transparent if it can be replaced with its value without changing the behavior of a program (in other words, yielding a program that has the same effects and

output on the same input).”

Voxxed Days Zürich 2016-03-03 9

Referen&al Transparency in Haskell

sum :: Int -> Int -> Intsum a b = a + b

sum 2 3 == 5sum 3 3 == 6

Voxxed Days Zürich 2016-03-03 10

Referen&al Transparency in Javapublic int sum(int a, int b) { int anotherB = // If the DB is empty, use b as default persistence.readFromSumTableWithDefault(b);

int sum = a + anotherB;

updateSumTable(sum);

return sum;}

sum(2, 3) == 5sum(2, 3) == 7 // ?!

Voxxed Days Zürich 2016-03-03 11

An Impure language can write a pure func2on

/* This is the same method as before */public int sum(int a, int b) { return a + b;}

sum(2, 3) == 5sum(2, 3) == 5

Voxxed Days Zürich 2016-03-03 12

This is not a pure func0on

public int sum(int a, int b) { writeLastExecutedFile(a, b); return a + b;}

Voxxed Days Zürich 2016-03-03 13

What are the "Pure" Func.onal programming languages?

• Haskell

• Clean

• Excel

• Scala?

• Java?

Voxxed Days Zürich 2016-03-03 14

Tes$ng pure code

Voxxed Days Zürich 2016-03-03 15

• By Paul Chiusano and Rúnar Bjarnason (Manning)Voxxed Days Zürich 2016-03-03 16

Untestable Cafeclass CreditCard() { def charge(price: BigDecimal): Unit = if (Random.nextInt(4) == 1) println("I'm calling a webservice") else throw new RuntimeException("crashing")}

case class Coffee(price: BigDecimal = 1)

class UntestableCafe { def buyCoffee(cc: CreditCard): Coffee = {

val cup = new Coffee() cc.charge(cup.price) // Side effect cup }}

Voxxed Days Zürich 2016-03-03 17

Untestable Cafe Test

class UntestableCafeTest extends Specification { "Cafe should be able to charge an amount" >> {

val cafe = new UntestableCafe()

val coffee = cafe.buyCoffee(new CreditCard())

success("hopefully charged") }}

Voxxed Days Zürich 2016-03-03 18

Testable Cafecase class CreditCard()case class Coffee(price: BigDecimal = 1)

trait Payments { def charge(cc: CreditCard, amount: BigDecimal) }

class TestableCafe { def buyCoffee(cc: CreditCard, p: Payments): Coffee = {

val cup = new Coffee()

p.charge(cc, cup.price) cup }}

Voxxed Days Zürich 2016-03-03 19

Testable Cafe Testclass TestableCafeTest extends Specification with Mockito { "Cafe should be able to charge an amount" >> {

val cafe = new TestableCafe()

val mockedPayment = mock[Payments] val cc = new CreditCard()

val coffee = cafe.buyCoffee(cc, mockedPayment)

there was one(mockedPayment).charge(cc, 1) }}

Voxxed Days Zürich 2016-03-03 20

Pure Cafecase class CreditCard()

case class Coffee(price: BigDecimal = 1)

case class Charge(cc: CreditCard, amount: BigDecimal)

class PureCafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = {

val cup = new Coffee()

(cup, Charge(cc, cup.price)) }}

Voxxed Days Zürich 2016-03-03 21

Pure Cafe Test

class PureCafeTest extends Specification with Mockito { "Cafe should be able to charge an amount" >> {

val cafe = new PureCafe()

val result = cafe.buyCoffee(new CreditCard())

result._2.amount === 1 }}

Voxxed Days Zürich 2016-03-03 22

Pure FP Helps Unit Tes0ng

Voxxed Days Zürich 2016-03-03 23

Do we have something be0er than unit tes3ng?

Voxxed Days Zürich 2016-03-03 24

Increase the entropy of the inputs"insert element in database" in { val username = randomString

val numberOfRowInserted = database.insert(1, username).futureAwait

val usernameFromDB = database.findById(1)

numberOfRowInserted === 1 usernameFromDB === username}

Voxxed Days Zürich 2016-03-03 25

Beware of pseudo-random tests!

Voxxed Days Zürich 2016-03-03 26

Property based tes-ng

val modelGenerator = for { tag <- arbitrary[String] elem <- arbitrary[String] } yield Model(tag, elem)

property("String is always 891 characters") = forAll(modelGenerator) { record => processString(record).length == 891}

Voxxed Days Zürich 2016-03-03 27

Proper&es instead of Asser&ons

Voxxed Days Zürich 2016-03-03 28

How can Pure FP helps Property based tes5ng?

Voxxed Days Zürich 2016-03-03 29

Beware of readability 1

take5 :: [Char] -> [Char]take5 = take 5 . filter (`elem` ['a'..'e'])

quickCheck ((\s -> (reverse.reverse) s == s) :: [Char] -> Bool)

quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))

1 h$ps://wiki.haskell.org/Introduc9ontoQuickCheck1

Voxxed Days Zürich 2016-03-03 30

TDD by Kent Beck and property base tes3ng 2

2 h$p://natpryce.com/ar2cles/000807.html

Voxxed Days Zürich 2016-03-03 31

Side Effects and Mocks

Voxxed Days Zürich 2016-03-03 32

Code Example with a lot of mocking public void mocking() { final File dir = Mockito.mock(File.class); BDDMockito.given(dir.getAbsolutePath()).willReturn("/some/otherdir/" + FILE); Mockito.doReturn(dir).when(extensionAccessor).getExtensionDir(FILE);

final String remotePath = "/some/dir/" + NAME; Mockito.doReturn(remotePath).when(request).getRequestURI();

Mockito.doReturn(remotePath).when(filter).getFullPathNameFromRequest(request); Mockito.doReturn("/some/dir/" + NAME).when(filter).getFullPathNameFromRequest(request);

filter.doFilter(request, response, filterChain);

Mockito.verify(filter).copyFileInternalIfNeeded("/some/dir/" + NAME); }

Voxxed Days Zürich 2016-03-03 33

Mockable classtrait DbThing { def getDbStuff(params): Future[DbStuff] }

trait HttpThing { def getHttpStuff(params): Future[HttpStuff] }

class BizThingImpl(db: DbThing, http: HttpThing) { def doComplicatedStuff(params): () = db.getDbStuff(params).zip(http.getHttpStuff(params)).map { case (dbStuff, httpStuff) => if(dbStuff.value > 0) doSomething else doSomethingElse case _ => throw new Exception("something went wrong") }}

Voxxed Days Zürich 2016-03-03 34

Can we avoid stubbing?def bizThingImpl(getDBStuff: Param => Future[DBStuff]) (httpStuff: Param => Future[HTTPStuff]): () = {

getDbStuff.zip(httpStuff(params)).map { case (dbStuff, httpStuff) => if(dbStuff.value > 0) doSomething else doSomethingElse case _ => throw new Exception("something went wrong") }}

Voxxed Days Zürich 2016-03-03 35

How can we avoid mocking?def bizThingImpl(getDBStuff: Param => Future[DBStuff]) (httpStuff: Param => Future[HTTPStuff]): Future[Result] = {

getDbStuff.zip(httpStuff(params)).map { case (dbStuff, httpStuff) => if(dbStuff.value > 0) Ok(httpValue.result) else NotFound case _ => InternalServerError }}

Voxxed Days Zürich 2016-03-03 36

Separate the pure from the impure part

Voxxed Days Zürich 2016-03-03 37

Applica'on with li.le logicpublic void complexInteraction(User user) {

httpClient.call(user.firstName , user.lastName, user.address, user.capCode, user.phoneNumber)

db.insertUser(user.firstName , user.lastName, user.address, user.capCode, user.phoneNumber)}

Voxxed Days Zürich 2016-03-03 38

Tes$ng applica$on with li1le logicpublic void testComplexInteraction(User user) {

User user = aUser("firstName", "lastName", "address", "capCode", "phoneNumber")

service.complexInteraction(user)

verify(httpClient) .call(user.firstName, user.lastName, user.address , user.capCode, user.phoneNumber) verify(db) .insertUser(user.firstName , user.lastName, user.address , user.capCode, user.phoneNumber)

}

Voxxed Days Zürich 2016-03-03 39

What should we test?

Voxxed Days Zürich 2016-03-03 40

It is hard to write tests.

Voxxed Days Zürich 2016-03-03 41

Tests vs Type Systems

Voxxed Days Zürich 2016-03-03 42

Referen&al Transparency again 3

function sum(a, b) = { ??? }

num = sum(2,3)

test.assert(typeof num === 'number');

3 h$p://unitjs.com/

Voxxed Days Zürich 2016-03-03 43

Same test using Javapublic String sayHello(String name) { return "Hello " + name + "!";}

public void testGreetingIsString() { assertEquals(String.class, sayHello("luca").getClass());}

Voxxed Days Zürich 2016-03-03 44

Excep&ons break referen&al transparency

Voxxed Days Zürich 2016-03-03 45

Feedback

Voxxed Days Zürich 2016-03-03 46

Compile )me vs Execu)on )me

• Type System ⟶ Compile /me

• Tests ⟶ Run Time

Voxxed Days Zürich 2016-03-03 47

Reading asser+on vs reading type errors

Voxxed Days Zürich 2016-03-03 48

Compiler error messages shouldn't be hard (Elm) 4

4 h$p://elm-lang.org/blog/compiler-errors-for-humans

Voxxed Days Zürich 2016-03-03 49

"The problem with the type checker is that it should never be smarter

than the developer"— Unknown

Voxxed Days Zürich 2016-03-03 50

Readability• Use unit tes*ng to give example of your API

• Use property based tes*ng when you want to verify proper*es

• Use Type Systems when having li?le logic

Voxxed Days Zürich 2016-03-03 51

Be careful using type inference

Voxxed Days Zürich 2016-03-03 52

BONUS

Voxxed Days Zürich 2016-03-03 53

Dependent Types

Voxxed Days Zürich 2016-03-03 54

Test the concatena+ons of two lists

def testConcatenationOfList() { List(1, 2) ++ List(3,4) === List(1,2,3,4)}

def testConcatenationOfListWithSize() { val sum = List(1, 2) ++ List(3,4)

sum.length === 4}

Voxxed Days Zürich 2016-03-03 55

Concatena(on using dependent types 5

concatenate : Vect n a -> Vect m a -> Vect (n + m) aconcatenate Nil ys = ysconcatenate (x :: xs) ys = x :: app xs ys

5 h$ps://eb.host.cs.st-andrews.ac.uk/wri8ngs/idris-tutorial.pdf

Voxxed Days Zürich 2016-03-03 56

Idris error messages

Can’t unify

Vect (n + n) awith

Vect (plus n m) aSpecifically:

Can’t unify plus n n with plus n m

Voxxed Days Zürich 2016-03-03 57

Conclusion• Pure Func*onal Programming leads to a more testable code

• If you're going to use pure FP, use a strong type system too (but don't forget tests)

• Modern type systems help us finding bugs in a more efficient way while designing our applica*on.

• If the logic is limited, rely on the Type System

Voxxed Days Zürich 2016-03-03 58

Q&A

Voxxed Days Zürich 2016-03-03 59

Thank you for your ,meFeedback please!

@volothamp

mailto:[email protected]

Voxxed Days Zürich 2016-03-03 60

BONUS 2

Voxxed Days Zürich 2016-03-03 61

Stability Problem• h#ps://github.com/mikeizbicki/HerbiePlugin

Voxxed Days Zürich 2016-03-03 62

The problem with the IO Type / Monad in Haskell

Voxxed Days Zürich 2016-03-03 63