Consider this simple abstraction for

`Money`

that keeps track of amounts in various currencies. scala> import Money._ import Money._ // 1000 USD scala> val m = Money(1000, USD) m: laws.Money = (USD,1000) // add 248 AUD scala> val n = add(m, Money(248, AUD)) n: laws.Money = (AUD,248),(USD,1000) // add 230 USD more scala> val p = add(n, Money(230, USD)) p: laws.Money = (AUD,248),(USD,1230) // value of the money in base currency (USD) scala> p.toBaseCurrency res1: BigDecimal = 1418.48 // debit amount scala> val q = Money(-250, USD) q: laws.Money = (USD,-250) scala> val r = add(p, q) r: laws.Money = (AUD,248),(USD,980)

The valuation of

`Money`

is done in terms of its base currency which is usually `USD`

. One of the possible implementations of `Money`

is the following (some parts elided for future explanations) ..sealed trait Currency case object USD extends Currency case object AUD extends Currency case object JPY extends Currency case object INR extends Currency class Money private[laws] (val items: Map[Currency, BigDecimal]) { def toBaseCurrency: BigDecimal = items.foldLeft(BigDecimal(0)) { case (a, (ccy, amount)) => a + Money.exchangeRateWithUSD.get(ccy).getOrElse(BigDecimal(1)) * amount } override def toString = items.toList.mkString(",") } object Money { final val zeroMoney = new Money(Map.empty[Currency, BigDecimal]) def apply(amount: BigDecimal, ccy: Currency) = new Money(Map(ccy -> amount)) def add(m: Money, amount: BigDecimal, ccy: Currency) = ??? final val exchangeRateWithUSD: Map[Currency, BigDecimal] = Map(AUD -> 0.76, JPY -> 0.009, INR -> 0.016, USD -> 1.0) }

Needless to say we will have quite a number of unit tests that check for addition of

`Money`

, including the boundary cases of adding to `zeroMoney`

.It's not very hard to see that the type

`Money`

forms a `Monoid`

under the `add`

operation. Or to speak a bit loosely we can say that `Money`

is a `Monoid`

under the `add`

operation.A

`Monoid`

has laws that every instance needs to honor - associativity, left identity and right identity. And when your model element needs to honor the laws of algebra, it's always recommended to include the verification of the laws as part of your test suite. Besides validating the sanity of your abstractions, one side-effect of verifying laws is that you can get rid of many of your explicitly written unit tests for the operation that forms the `Monoid`

. They will be automatically verified when verifying the laws of `Monoid[Money]`

.Here's how we define

`Monoid[Money]`

using Cats ..val MoneyAddMonoid: Monoid[Money] = new Monoid[Money] { def combine(m: Money, n: Money): Money = add(m, n) def empty: Money = zeroMoney }

and the implementation of the previously elided add operation on

`Money`

using `Monoid`

on `Map`

..object Money { //.. def add(m: Money, amount: BigDecimal, ccy: Currency) = new Money(m.items |+| Map(ccy -> amount)) //.. }

Now we can verify the laws of

`Monoid[Money]`

using specs2 and ScalaCheck and the helper classes that Cats offers ..import cats._ import kernel.laws.GroupLaws import org.scalacheck.{ Arbitrary, Gen } import Arbitrary.arbitrary class MoneySpec extends CatsSpec { def is = s2""" This is a specification for validating laws of Money (Money) should form a monoid under addition $e1 """ implicit lazy val arbCurrency: Arbitrary[Currency] = Arbitrary { Gen.oneOf(AUD, USD, INR, JPY) } implicit def moneyArbitrary: Arbitrary[Money] = Arbitrary { for { i <- Arbitrary.arbitrary[Map[Currency, BigDecimal]] } yield new Money(i) } def e1 = checkAll("Money", GroupLaws[Money].monoid(Money.MoneyAddMonoid)) }

and running the test suite will verify the Monoid laws for

`Monoid[Money]`

..
[info] This is a specification for validating laws of Money

[info]

[info] (Money) should

[info] form a monoid under addition monoid laws must hold for Money

[info] + monoid.associativity

[info] + monoid.combineAll

[info] + monoid.combineAll(Nil) == id

[info] + monoid.combineAllOption

[info] + monoid.combineN(a, 0) == id

[info] + monoid.combineN(a, 1) == a

[info] + monoid.combineN(a, 2) == a |+| a

[info] + monoid.isEmpty

[info] + monoid.leftIdentity

[info] + monoid.rightIdentity

[info] + monoid.serializable

In summary ..

- strive to find abstractions in your domain model that are constrained by algebraic laws
- check all laws as part of your test suite
- you will find that you can get rid of quite a few explicitly written unit tests just by checking the laws of your abstraction
- and of course use property based testing for unit tests

## No comments:

Post a Comment