One of the most important things to get right with my regexModeler application was the test suite, because I made several breaking changes many times along the way and would have been dead in the water without one. (Quick recap: it takes in a regular expression and then spits out a randomly-generated string which would match it.)
Because of all the random aspects of the program, it was an early challenge to get something for the unit tests to measure. I handled this by abstracting the parts of the program which (1) retrieved a character set from which to choose, and (2) elected a character from such a set. This allowed me to create stub objects which I could then populate as needed with deterministic versions of the functions that were being hit by the tested parts of the program. Now, if this is something you need to do too, bear in mind that object expressions can do much the same thing, but the limitation is that you need to define all of the methods for your solution to build. By taking the approach I outline below, you have more flexibility to define just the behavior you need to, while safely ignoring the rest. Also, if you want to take it a step further to add features similar to mocking libraries, you can do that.
A simple example comes from the character class feature. Here's part of the interface for the CharGenerator type, which holds the responsibility for returning one or more characters that meet certain criteria:
namespace RegexModeler.Interfaces type ICharGenerator = abstract member GetNDigits : int -> char list abstract member GetNNonDigits : int -> char list abstract member GetNWordChars : int -> char list abstract member GetNNonWordChars : int -> char list abstract member GetNSpaceChars : int -> char list abstract member GetNNonSpaceChars : int -> char list
And here's the concrete implementation of getNCharsFromClass, a very simple matching function in CharClass that uses charGenerator to return characters which meet the different qualities specified by a shorthand character class:
type CharClass(charGenerator, numGenerator) = let charGenerator = charGenerator :> ICharGenerator interface ICharClass with member _x.getNCharsFromClass n key = match key with | 'd' -> charGenerator.GetNDigits n | 'D' -> charGenerator.GetNNonDigits n | 'w' -> charGenerator.GetNWordChars n | 'W' -> charGenerator.GetNNonWordChars n | 'b' | 's' -> charGenerator.GetNSpaceChars n | 'B' | 'S' -> charGenerator.GetNNonSpaceChars n | _ -> raise <| InvalidShorthandClassException "Unsupported shorthand character class"
Here's the stub object for CharGenerator. Note that it's called a stub and not a mock, since I'm pretty sure that's what it is, although lots of people call virtually any test double a "mock" and you might be among them which is totally fine:
type CharGeneratorStub(?GetNDigits, ?GetNNonDigits, ?GetNWordChars, ?GetNNonWordChars, ?GetNSpaceChars, ?GetNNonSpaceChars) = member private _x.GetNListItems = CreateStub GetNListItems "GetNListItems" member private _x.GetNDigits = CreateStub GetNDigits "GetNDigits" member private _x.GetNNonDigits = CreateStub GetNNonDigits "GetNNonDigits" member private _x.GetNWordChars = CreateStub GetNWordChars "GetNWordChars" member private _x.GetNNonWordChars = CreateStub GetNNonWordChars "GetNNonWordChars" member private _x.GetNSpaceChars = CreateStub GetNSpaceChars "GetNSpaceChars" member private _x.GetNNonSpaceChars = CreateStub GetNNonSpaceChars "GetNNonSpaceChars" interface ICharGenerator with member x.GetNDigits n = x.GetNDigits n member x.GetNNonDigits n = x.GetNNonDigits n member x.GetNWordChars n = x.GetNWordChars n member x.GetNNonWordChars n = x.GetNNonWordChars n member x.GetNSpaceChars n = x.GetNSpaceChars n member x.GetNNonSpaceChars n = x.GetNNonSpaceChars n
And the CreateStub helper it uses:
let CreateStub fn msg = match fn with | Some fn -> fn | None -> raise <| RegexModeler.UnimplementedMemberException msg
So there's nothing revolutionary here, and you can pretty easily see how the whole thing works. We first defer responsibility for doing the non-deterministic stuff to a separate type and create an interface for it. Next we build a stub class to match the interface. This has a constructor which can take in any (or all) of the methods on that interface as optional parameters. Inside the stub class we use the CreateStub function to assign either a default function or the passed-in function to each of the members on the stub.
The result is that if you were to instantiate a new CharGeneratorStub with no parameters and pass it to the CharClass object, you would get exceptions for every usage of the methods on the CharClass--but the solution would still build. So if you do red-green unit testing, that's sort of your beginning. Now to make the unit tests pass, you will have to craft some deterministic result for the methods being hit and furnish those to the stub (before you do the rest of the work to actually make the test pass, which I can't really help with, from here). Here's how that's done in one of the regexModeler tests:
type EscapeModeTests () = member _x.GetEscape(?quantifier, ?charGenerator, ?charClass) = let quantifier' = defaultArg quantifier (new QuantifierStub() :> IQuantifier) let charGenerator' = defaultArg charGenerator (new CharGeneratorStub() :> ICharGenerator) let charClass' = defaultArg charClass (new CharClassStub() :> ICharClass) new EscapeMode(quantifier', charGenerator', charClass') [<Test>] member x.``processEscape, when given char class and no quantifier, returns one valid char``() = let quantifierStub = new QuantifierStub(processQuantifierFn = (fun _c -> (1, ['b'; 'o'; 'o']))) let charClassStub = new CharClassStub(getNCharsFromClass = (fun _i _c -> ['x'])) let escape = x.GetEscape(quantifierStub, charClass = charClassStub) let input = stringToChrs @"dboo" let expected = (['x'], ['b'; 'o'; 'o']) let actual = escape.processInMode input Assert.PairsEqual expected actual
And that's it. As I sort of mentioned before, you can keep going with this, and create a more robust framework that gives you more information about how often methods are called, whether they're called, and so on, but this was a good start for me. In any case I don't usually use those other features of, for example, Moq--they sort of creep into testing the internals too much for me--but that's a post for another day.