Successfully reported this slideshow.
Your SlideShare is downloading. ×

Testing in the World of Functional Programming

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 37 Ad
Advertisement

More Related Content

Slideshows for you (20)

Similar to Testing in the World of Functional Programming (20)

Advertisement

Recently uploaded (20)

Advertisement

Testing in the World of Functional Programming

  1. 1. Testing in the world of FP Luka Jacobowitz - Lamdba World 2018
  2. 2. 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
  3. 3. Agenda ● Property-based testing ● Mocking ● Conclusions
  4. 4. 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
  5. 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. 6. 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
  7. 7. scala-newtype @newtype case class Euros(n: Int) object Euros { implicit val eurosNumeric: Numeric[Euros] = deriving } Euros(25) + Euros(10) - Euros(5)
  8. 8. 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
  9. 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. 10. Accepting only valid inputs for your functions makes your code more precise and allows for much easier property-based testing
  11. 11. 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")
  12. 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. 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. 14. How do I test this? def sendXml(s: String): Unit def sendAll(list: List[String]): Unit = { list.foreach(sendXml) }
  15. 15. 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)
  16. 16. Now we can test parsing, what about sending? def sendXml(x: Xml): IO[Unit]
  17. 17. 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)
  18. 18. How do we use mocking to test IO? Tagless Final
  19. 19. 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
  20. 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. 21. An example def bookThings: IO[Unit] = for { _ <- bookDrink(coke) _ <- bookRoomService _ <- bookSandwich(wholeWheat, hummus) } yield ()
  22. 22. 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)
  23. 23. 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))) }
  24. 24. 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.
  25. 25. 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.
  26. 26. 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?
  27. 27. 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
  28. 28. A complex example trait Logging[F[_]] { def logCustomer(c: Customer): F[Unit] def logDiscount(dt: DiscountType, d: Discount): F[Unit] }
  29. 29. 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] }
  30. 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. 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. 32. 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] = … }
  33. 33. 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) } }
  34. 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. 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. 36. 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
  37. 37. Thank you for listening! Twitter: @LukaJacobowitz GitHub: LukaJCB

×