data Risk = PENDING | NORMAL | RISKY | REJECTED
derive Eq Risk
derive Show Risk
derive Enum Risk
data Loan = Loan { name :: Maybe String , amount :: Double, risk :: Risk }
derive Show Loan
Frege and QuickCheck: Combine properties
Sometimes our function should follow more than one property at the same time. How can I check more than one property in a given specification?
Combining properties
In the following example we’re pretending to be a small financial institution that lends money.
So we have a Loan
:
This is an example, please don’t use this data as reference for any financial development :P |
We need to build a function to process an incoming loan. At this point we only know which are the properties to reject a loan. It will be rejected:
-
When it has a negative amount
-
When it goes outside the company defined boundaries
(min: 0, max: 100000) -
When it has no name
Lets define these properties:
rejectNegative :: Loan -> Bool
rejectNegative loan = if isNegative
then statusResult == Risk.REJECTED
else true
where isNegative = loan.amount < 0
loanResult = calculateRisk loan
statusResult = loanResult.risk
rejectBeyondMax :: Loan -> Bool
rejectBeyondMax loan = if isBeyondMax
then statusResult == Risk.REJECTED
else true
where isBeyondMax = loan.amount > 100_000
loanResult = calculateRisk loan
statusResult = loanResult.risk
rejectAnonymous :: Loan -> Bool
rejectAnonymous loan = if isAnonymous
then statusResult == Risk.REJECTED
else true
where isAnonymous = loan.name == Nothing
loanResult = calculateRisk loan
statusResult = loanResult.risk
Lets see how we’ve defined our function with these requirements in mind:
calculateAmountRisk :: Loan -> Loan (1)
calculateAmountRisk loan
| inRange (1,9999) loan.amount = loan.{ risk = NORMAL }
| inRange (10_000,100_000) loan.amount = loan.{ risk = RISKY }
| otherwise = loan.{ risk = REJECTED }
calculateAnonymousRisk :: Loan -> Loan (2)
calculateAnonymousRisk loan = case loan of
Loan Nothing _ _ -> loan.{ risk = REJECTED }
_ -> loan
calculateRisk :: Loan -> Loan (3)
calculateRisk loan = (calculateAnonymousRisk . calculateAmountRisk) loan
1 | Function to calculate risk based on the loan ammount |
2 | Function to calculate risk based on the loan name |
3 | Function combining previous two functions |
Please notice I’m not mutating the data structure when
doing loan.{ risk = Risk.REJECTED} . In this case, changing a field
means copying the data structure and setting the new value in the new
copy.
|
Ok, lets see if the function holds for these properties:
checkName = property rejectAnonymous
checkMin = property rejectNegative
checkMax = property rejectBeyondMax
qc.CombineCheck.checkName: +++ OK, passed 100 tests
qc.CombineCheck.checkMin: +++ OK, passed 100 tests
qc.CombineCheck.checkMax: +++ OK, passed 100 tests
So far we have tested our properties isolated, but in the real world I would make sure that a given loan passes those properties all at once. How do I do that ?
Well there’s a function called conjoin
which takes care of it:
calculateRiskCheck = conjoin [rejectNegative, rejectBeyondMax, rejectAnonymous]
qc.CombineCheck.calculateRiskCheck: +++ OK, passed 100 tests
Now I’m sure the loan passes if all properties over a loan pass the test.