what is pure functional programming, and how it can improve our application testing?
TRANSCRIPT
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
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 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
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
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
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
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
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
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
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
Compile )me vs Execu)on )me
• Type System ⟶ Compile /me
• Tests ⟶ Run Time
Voxxed Days Zürich 2016-03-03 47
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
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
Thank you for your ,meFeedback please!
@volothamp
mailto:[email protected]
Voxxed Days Zürich 2016-03-03 60