Almost a year ago now, I wrote up a blog post on using FsCheck. I still rate it as an excellent tool, but unfortunately we don't manage to use it that much. The reasons for this basically boil down to the fact that a) we tend to forget it exists and b) a good deal of our code is written in C# or VB.net, and the original API is not very friendly from those languages.
So as part of the 15below developer education sessions we're going to try an exercise to see if we can bring a bit more property based testing into our code base!
Never trust the user…
One of the things we do quite a lot of as a company is sending either automated voice calls or SMS messages. The phone number we're trying to contact is often free text provided by the customer, while the voice/SMS companies tend to be very keen on phone numbers that are in (something at least similar to) the international E.164 phone number format.
Unfortunately, users don't tend to very good at sticking to standards in free text fields - so it some point your code needs to make the call about whether you're convinced the phone number you have is valid or not…
For the exercise, I've created idiomatic stubs of a PhoneNumber class in both F# and C# with methods for creating them that check if the input string is valid. The C# version uses PhoneNumber.TryParse:
moduleFSharp.FsCheck.PhoneNumberopenSystem.Text.RegularExpressionstypePossibleNumber={CountryCode:intIdentificationCode:intSubscriberNumber:int}typePhoneNumber=|ValidPhoneNumberofPossibleNumber|InvalidPhoneNumberofstring// Shadow the name so that no one else// can create "ValidPhoneNumber"letValidPhoneNumberinput=letreg=Regex(@"\+(?<cc>\d+) (?<ic>\d+) (?<sn>\d+)")matchreg.IsMatch(input)with|true->letgroups=reg.Match(input).GroupsValidPhoneNumber{CountryCode=groups.["cc"].Value|>intIdentificationCode=groups.["ic"].Value|>intSubscriberNumber=groups.["sn"].Value|>int}|false->InvalidPhoneNumber"No good"
The challenge will be to use property checking to take the stub to a class that fulfils the following properties:
Country code between 1 and 3 digits
Identification code 4 or less digits (may be missing)
Subscription number between 1 and (15 - country code - identification code) digits
Less than 15 total digits
These all come straight from the specification - we're going to ignore country groups for now.
Each of the two projects also includes a PropertyChecks file that contains the skeleton of an NUnit based FsCheck test suite. We only have an hour for our DevEd sessions, so the project includes a reasonable amount to get you going. Each one has a "sanity check" test with a known good phone number, and property based checks for the length of the country code and whether all valid numbers are recognised as valid. To make the second property test work, they also both include a custom
generator for valid phone numbers.
moduleFSharp.FsCheck.PropertyChecksopenFsCheckopenNUnit.FrameworkopenPhoneNumbertypeGeneratedValidNumber={Country:intIdentifier:intoptionSubscriber:intInputString:string}letvalidNumberGen=gen{let!c=Gen.choose(1,999)let!i=Gen.oneof[gen{let!i=Gen.choose(1,9999)return(Somei)}gen{returnNone}]letmaxSubLength=float<|15-(c.ToString().Length)-(matchiwith|None->0|Somex->x.ToString().Length)let!s=Gen.choose(1,(int<|10.**maxSubLength)-1)return{Country=cIdentifier=iSubscriber=sInputString=sprintf"+%d%s %d"c(matchiwith|None->""|Somex->sprintf" %d"x)s}}typePhoneNumberGenerators=staticmemberValid()={newArbitrary<GeneratedValidNumber>()withoverridex.Generator=validNumberGen}[<Test>]let``Sanitycheck``()=matchValidPhoneNumber"+44 1234 123456"with|ValidPhoneNumbern->Assert.AreEqual(n.CountryCode,44)Assert.AreEqual(n.IdentificationCode,1234)Assert.AreEqual(n.SubscriberNumber,123456)|InvalidPhoneNumber_->Assert.Fail()[<Test>]let``Insanitycheck``()=matchValidPhoneNumber"I'm not a phone number"with|ValidPhoneNumbern->Assert.Fail()|InvalidPhoneNumber_->()[<Test>]let``Countrycodelessthan4digits``()=letgenNumber(DontSize(cc:uint32))=matchValidPhoneNumber("+"+cc.ToString()+" 1234 123456")with|ValidPhoneNumbern->Assert.IsTrue(n.CountryCode.ToString().Length<4)|InvalidPhoneNumber_->()Check.QuickThrowOnFailuregenNumber[<Test>]let``Validnumbersarecountedasvalid``()=Arb.register<PhoneNumberGenerators>()|>ignoreCheck.VerboseThrowOnFailure(fun(v:GeneratedValidNumber)->matchValidPhoneNumberv.InputStringwith|ValidPhoneNumber_->true|InvalidPhoneNumber_->false)
These run fine as NUnit tests - apart from the fact that in true TDD style, they fail.
The challenge!
So, the challenge (which is open to people outside 15below as well). Basically, fork the git repository and then check out locally. This contains everything, including both projects and the binaries of all their dependencies to avoid any NuGet issues. Within 15below, we'll be working in pairs - otherwise when you're sitting at your own computer with "real work" to do, it's very hard to actually take the hour out on the exercise.
In the order of your choice:
Add property checks for the missing properties above
Update the PhoneNumber class to pass all of the tests
Extra credit: Add a generator for local numbers from a known country (i.e. the UK) and property test your conversion method
Extra credit 2: complete any of all of the above in both F# and C#
Completely carried away: pick a real piece of production code and add a property test to it…
Once you've got as far as you're going to, commit your changes and push back up to GitHub, then send a pull request with progress back to the parent repository. I won't merge these, but the different implementations of both the phone number class and property tests will form the basis of the DevEd session the week after, possibly with votes for the most elegant/robust solutions. If you're not a member of staff here at 15below, I'll try and update your pull request with any
feedback from our discussions!