Testing in the world of FP Luka Jacobowitz - Lamdba World 2018
Software Developer at codecentric Co-organizer of ScalaDus and IdrisDus Maintainer of cats, cats-effect, cats-mtl, cats-tagless, OutWatch Enthusiastic about FP About me
Agenda ● Property-based testing ● Mocking ● Conclusions
Property-based testing is awesome ● It allows us to generate a bunch of test cases we would never have been able to write by ourselves ● Makes sure even edge cases are well handled ● Can help us find bugs as early as possible
Property-based testing - Pains /** * Should use UUIDv1 and "yyyy-MM-dd HH:mm:ss" format */ def uuidCreatedAfter(uuid: String, date: String): Boolean = { val arr = uuid.split("-") val timestampHex = arr(2).substring(1) + arr(1) + arr(0) ... }
Newtype it! Common reasons against newtyping: ● It won’t support the operations I need ● It will lead to a lot of boilerplate conversions ● It will give us a performance penalty
scala-newtype @newtype case class Euros(n: Int) object Euros { implicit val eurosNumeric: Numeric[Euros] = deriving } Euros(25) + Euros(10) - Euros(5)
Refined val url: String Refined Url = "http://example.com" val failed: String Refined Url = "hrrp://example.com" error: Url predicate failed: unknown protocol: hrrp val n: Int Refined Positive = 42
Refined-Scalacheck import eu.timepit.refined.scalacheck.numeric._ def leftPad(s: String, n: Int Refined Positive): String forAll { (s: String, n: Int Refined Positive) => assert(leftPad(s, n).length >= n) }
Accepting only valid inputs for your functions makes your code more precise and allows for much easier property-based testing
So many more gems out there val n: Int Refined Positive = NonEmptyList.of(1, 2, 3).refinedSize val v: ValidatedNel[String, SHA1] = SHA1.validate("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12")
Use this instead ● A duration of time -> use FiniteDuration instead of Int ● A non-empty sequence -> use NonEmptyList instead of List ● A date -> use LocalDate instead of String ● An IP-address -> use String Refined IPv4 instead of String ● etc.
Other great libraries ● Libra - A dimensional analysis library, allows us to multiply two meter values and receive a square meter value ● FUUID - Functional UUID library, gives us new random UUIDs in IO and uses Either for construction from String ● Squants - Similar to Libra, but also comes with built in units of measure such as KiloWatts, Tons and even Money
How do I test this? def sendXml(s: String): Unit def sendAll(list: List[String]): Unit = { list.foreach(sendXml) }
Split it up def parseXml(s: String): Either[Throwable, Xml] def sendXml(x: Xml): IO[Unit] def parseAndSend(s: String): IO[Unit] = IO.fromEither(parseXml(s)).flatMap(sendXml) list.traverse_(parseAndSend)
Now we can test parsing, what about sending? def sendXml(x: Xml): IO[Unit]
Testing what we don’t control We have two (non mutually exclusive) options 1. Use integration tests to check if the behaviour does what we want (can be difficult) 2. Mock the outside world and test expectations (easier, but potentially less accurate)
How do we use mocking to test IO? Tagless Final
Tagless Final ● Allows us to separate problem description from the actual problem solution and implementation ● This means we can use our own Algebras for defining interactions ● We can work at an extra level of abstraction but maintain flexibility
Tagless Final - How to ● Model our Algebras as traits parametrized with a type constructor ● Programs constrain the type parameter (e.g. with Monad) ● Interpreters are simply implementations of those traits
An example def bookThings: IO[Unit] = for { _ <- bookDrink(coke) _ <- bookRoomService _ <- bookSandwich(wholeWheat, hummus) } yield ()
Using Tagless Final trait BookingService[F[_]] { def bookDrink(b: Beverage): F[Unit] def bookRoomService: F[Unit] def bookSandwich(d: Dough, t: Topping): F[Unit] } def bookThings[F[_]: Apply](F: BookingService[F]): F[Unit] = F.bookDrink(coke) *> F.bookRoomService *> F.bookSandwich(wholeWheat, hummus)
A possible Test interpreter sealed trait Booking case class DrinkBooking(b: Beverage) extends Booking case object RoomServiceBooking extends Booking case class SandwichBooking(d: Dough, t: Topping) extends Booking def testService = new BookingService[Const[List[Booking], ?]] { def bookDrink(b: Beverage): Const[List[Booking], Unit] = Const(List(DrinkBooking(b))) def bookRoomService: Const[List[Booking], Unit] = Const(List(RoomServiceBooking)) def bookSandwich(d: Dough, t: Topping): Const[List[Booking], Unit] = Const(List(SandwichBooking(d, t))) }
Running the program val bookings: List[Booking] = bookThings(testService).getConst val expectedBookings: List[Booking] = List(...) assert(bookings === expectedBookings) What are we testing here? The external system? We’re only testing the interal wiring of our own system.
External world testing continued Last time was too easy, we were just returning Unit! Usually we have to deal with more complex return types that are much harder to mock.
External world testing continued Two questions: 1. What does your external service provide to you, is it just data or also State? 2. Can you reasonably mimic the behaviour of the external service with a self-contained state machine?
A complex example def discountAll(dt: DiscountType): IO[List[Customer]] = for { customers <- getAllCustomers _ <- customers.traverse_(logCustomer) discount <- getDiscount(dt) _ <- logDiscount(dt, discount) updated <- customers.traverse(c => updateDiscountIfEligible(discount, c)) _ <- updated.traverse_(logCustomer) } yield updated
A complex example trait Logging[F[_]] { def logCustomer(c: Customer): F[Unit] def logDiscount(dt: DiscountType, d: Discount): F[Unit] }
A complex example trait CustomerService[F[_]] { def getAllCustomers: F[List[Customer]] def getDiscount(dt: DiscountType): F[Discount] def updateDiscountIfEligible(d: Discount, c: Customer): F[Customer] }
def discountAll[F[_]: Monad](dt: DiscountType) (F: CustomerService[F], L: Logging[F]): F[List[Customer]] = for { customers <- F.getAllCustomers _ <- customers.traverse_(L.logCustomer) discount <- F.getDiscount(dt) _ <- L.logDiscount(dt, discount) updated <- customers.traverse(c => F.updateDiscountIfEligible(discount, c)) _ <- updated.traverse_(L.logCustomer) } yield updated A complex example
def isEligible(d: Discount, c: Customer): Boolean def updateCustomer(d: Discount, c: Customer): Customer case class ServiceState(customers: List[Customer], discounts: Map[DiscountType, Discount]) A complex example
A complex example val testInterp = new CustomerService[State[ServiceState, ?]] { def getAllCustomers: State[ServiceState, List[Customer]] = State.get[ServiceState].map(_.customers) def getDiscount(dt: DiscountType): State[ServiceState, Discount] = State.get[ServiceState].map(_.discounts(dt)) def updateDiscountIfEligible(d: Discount, c: Customer): State[ServiceState, Customer] = … }
A complex example def updateDiscountIfEligible(d: Discount, c: Customer): State[ServiceState, Customer] = State { s => if (isEligible(d, c)) { val updated = updateCustomer(d, c) val withoutCustomer = s.customers.filter(_ === c) (s.copy(customers = updated +: withoutCustomer), updated) } else { (s, c) } }
A complex example forAll { (customers: List[Customer], i: Item) => val d: Discount = Discount.Fix(0.2, List.empty) val expected: Price = customers .filter(c => isEligible(d, c)).foldMap(i.priceFor).discountBy(d) val state: State[ServiceState, List[Customer]] = discountAll(DiscountType.Fix)(testInterp, testLogger) val initial = ServiceState(customers, Map(DiscountType.Fix -> d)) val price = state.runA(ServiceState(customers)).value.foldMap(i.priceFor) assert(price === expected) }
Testing external effects - recap ● If the situation allows it, we can mock the behaviour of an external service and therefore pull it into our world, making it fully deterministic ● We have to evaluate on a case by case basis if this feasible or worth doing ● If done right, it can give us more confidence in our testing
Conclusions ● Separate effectful code from pure code ● Make use of total functions with well defined inputs as much as possible ● When testing side-effects, see if you can mock some of the behaviour
Thank you for listening! Twitter: @LukaJacobowitz GitHub: LukaJCB

Testing in the World of Functional Programming

  • 1.
    Testing in theworld of FP Luka Jacobowitz - Lamdba World 2018
  • 2.
    Software Developer at codecentric Co-organizerof ScalaDus and IdrisDus Maintainer of cats, cats-effect, cats-mtl, cats-tagless, OutWatch Enthusiastic about FP About me
  • 3.
    Agenda ● Property-basedtesting ● Mocking ● Conclusions
  • 4.
    Property-based testing isawesome ● It allows us to generate a bunch of test cases we would never have been able to write by ourselves ● Makes sure even edge cases are well handled ● Can help us find bugs as early as possible
  • 5.
    Property-based testing -Pains /** * Should use UUIDv1 and "yyyy-MM-dd HH:mm:ss" format */ def uuidCreatedAfter(uuid: String, date: String): Boolean = { val arr = uuid.split("-") val timestampHex = arr(2).substring(1) + arr(1) + arr(0) ... }
  • 6.
    Newtype it! Common reasonsagainst newtyping: ● It won’t support the operations I need ● It will lead to a lot of boilerplate conversions ● It will give us a performance penalty
  • 7.
    scala-newtype @newtype case classEuros(n: Int) object Euros { implicit val eurosNumeric: Numeric[Euros] = deriving } Euros(25) + Euros(10) - Euros(5)
  • 8.
    Refined val url: StringRefined Url = "http://example.com" val failed: String Refined Url = "hrrp://example.com" error: Url predicate failed: unknown protocol: hrrp val n: Int Refined Positive = 42
  • 9.
    Refined-Scalacheck import eu.timepit.refined.scalacheck.numeric._ def leftPad(s:String, n: Int Refined Positive): String forAll { (s: String, n: Int Refined Positive) => assert(leftPad(s, n).length >= n) }
  • 10.
    Accepting only validinputs for your functions makes your code more precise and allows for much easier property-based testing
  • 11.
    So many moregems out there val n: Int Refined Positive = NonEmptyList.of(1, 2, 3).refinedSize val v: ValidatedNel[String, SHA1] = SHA1.validate("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12")
  • 12.
    Use this instead ●A duration of time -> use FiniteDuration instead of Int ● A non-empty sequence -> use NonEmptyList instead of List ● A date -> use LocalDate instead of String ● An IP-address -> use String Refined IPv4 instead of String ● etc.
  • 13.
    Other great libraries ●Libra - A dimensional analysis library, allows us to multiply two meter values and receive a square meter value ● FUUID - Functional UUID library, gives us new random UUIDs in IO and uses Either for construction from String ● Squants - Similar to Libra, but also comes with built in units of measure such as KiloWatts, Tons and even Money
  • 14.
    How do Itest this? def sendXml(s: String): Unit def sendAll(list: List[String]): Unit = { list.foreach(sendXml) }
  • 15.
    Split it up defparseXml(s: String): Either[Throwable, Xml] def sendXml(x: Xml): IO[Unit] def parseAndSend(s: String): IO[Unit] = IO.fromEither(parseXml(s)).flatMap(sendXml) list.traverse_(parseAndSend)
  • 16.
    Now we cantest parsing, what about sending? def sendXml(x: Xml): IO[Unit]
  • 17.
    Testing what wedon’t control We have two (non mutually exclusive) options 1. Use integration tests to check if the behaviour does what we want (can be difficult) 2. Mock the outside world and test expectations (easier, but potentially less accurate)
  • 18.
    How do weuse mocking to test IO? Tagless Final
  • 19.
    Tagless Final ● Allowsus to separate problem description from the actual problem solution and implementation ● This means we can use our own Algebras for defining interactions ● We can work at an extra level of abstraction but maintain flexibility
  • 20.
    Tagless Final -How to ● Model our Algebras as traits parametrized with a type constructor ● Programs constrain the type parameter (e.g. with Monad) ● Interpreters are simply implementations of those traits
  • 21.
    An example def bookThings:IO[Unit] = for { _ <- bookDrink(coke) _ <- bookRoomService _ <- bookSandwich(wholeWheat, hummus) } yield ()
  • 22.
    Using Tagless Final traitBookingService[F[_]] { def bookDrink(b: Beverage): F[Unit] def bookRoomService: F[Unit] def bookSandwich(d: Dough, t: Topping): F[Unit] } def bookThings[F[_]: Apply](F: BookingService[F]): F[Unit] = F.bookDrink(coke) *> F.bookRoomService *> F.bookSandwich(wholeWheat, hummus)
  • 23.
    A possible Testinterpreter sealed trait Booking case class DrinkBooking(b: Beverage) extends Booking case object RoomServiceBooking extends Booking case class SandwichBooking(d: Dough, t: Topping) extends Booking def testService = new BookingService[Const[List[Booking], ?]] { def bookDrink(b: Beverage): Const[List[Booking], Unit] = Const(List(DrinkBooking(b))) def bookRoomService: Const[List[Booking], Unit] = Const(List(RoomServiceBooking)) def bookSandwich(d: Dough, t: Topping): Const[List[Booking], Unit] = Const(List(SandwichBooking(d, t))) }
  • 24.
    Running the program valbookings: List[Booking] = bookThings(testService).getConst val expectedBookings: List[Booking] = List(...) assert(bookings === expectedBookings) What are we testing here? The external system? We’re only testing the interal wiring of our own system.
  • 25.
    External world testingcontinued Last time was too easy, we were just returning Unit! Usually we have to deal with more complex return types that are much harder to mock.
  • 26.
    External world testingcontinued Two questions: 1. What does your external service provide to you, is it just data or also State? 2. Can you reasonably mimic the behaviour of the external service with a self-contained state machine?
  • 27.
    A complex example defdiscountAll(dt: DiscountType): IO[List[Customer]] = for { customers <- getAllCustomers _ <- customers.traverse_(logCustomer) discount <- getDiscount(dt) _ <- logDiscount(dt, discount) updated <- customers.traverse(c => updateDiscountIfEligible(discount, c)) _ <- updated.traverse_(logCustomer) } yield updated
  • 28.
    A complex example traitLogging[F[_]] { def logCustomer(c: Customer): F[Unit] def logDiscount(dt: DiscountType, d: Discount): F[Unit] }
  • 29.
    A complex example traitCustomerService[F[_]] { def getAllCustomers: F[List[Customer]] def getDiscount(dt: DiscountType): F[Discount] def updateDiscountIfEligible(d: Discount, c: Customer): F[Customer] }
  • 30.
    def discountAll[F[_]: Monad](dt:DiscountType) (F: CustomerService[F], L: Logging[F]): F[List[Customer]] = for { customers <- F.getAllCustomers _ <- customers.traverse_(L.logCustomer) discount <- F.getDiscount(dt) _ <- L.logDiscount(dt, discount) updated <- customers.traverse(c => F.updateDiscountIfEligible(discount, c)) _ <- updated.traverse_(L.logCustomer) } yield updated A complex example
  • 31.
    def isEligible(d: Discount,c: Customer): Boolean def updateCustomer(d: Discount, c: Customer): Customer case class ServiceState(customers: List[Customer], discounts: Map[DiscountType, Discount]) A complex example
  • 32.
    A complex example valtestInterp = new CustomerService[State[ServiceState, ?]] { def getAllCustomers: State[ServiceState, List[Customer]] = State.get[ServiceState].map(_.customers) def getDiscount(dt: DiscountType): State[ServiceState, Discount] = State.get[ServiceState].map(_.discounts(dt)) def updateDiscountIfEligible(d: Discount, c: Customer): State[ServiceState, Customer] = … }
  • 33.
    A complex example defupdateDiscountIfEligible(d: Discount, c: Customer): State[ServiceState, Customer] = State { s => if (isEligible(d, c)) { val updated = updateCustomer(d, c) val withoutCustomer = s.customers.filter(_ === c) (s.copy(customers = updated +: withoutCustomer), updated) } else { (s, c) } }
  • 34.
    A complex example forAll{ (customers: List[Customer], i: Item) => val d: Discount = Discount.Fix(0.2, List.empty) val expected: Price = customers .filter(c => isEligible(d, c)).foldMap(i.priceFor).discountBy(d) val state: State[ServiceState, List[Customer]] = discountAll(DiscountType.Fix)(testInterp, testLogger) val initial = ServiceState(customers, Map(DiscountType.Fix -> d)) val price = state.runA(ServiceState(customers)).value.foldMap(i.priceFor) assert(price === expected) }
  • 35.
    Testing external effects- recap ● If the situation allows it, we can mock the behaviour of an external service and therefore pull it into our world, making it fully deterministic ● We have to evaluate on a case by case basis if this feasible or worth doing ● If done right, it can give us more confidence in our testing
  • 36.
    Conclusions ● Separate effectfulcode from pure code ● Make use of total functions with well defined inputs as much as possible ● When testing side-effects, see if you can mock some of the behaviour
  • 37.
    Thank you forlistening! Twitter: @LukaJacobowitz GitHub: LukaJCB