Mike McClurg

Archive for the ‘OCaml’ Category

OCaml Design Pattern: Easy functorization refactoring for unit testing

leave a comment »

I’ve recently stumbled upon a useful OCaml design pattern for functorizing an existing module, without changing the way existing clients of that module use it. This is really useful for stubbing out dependencies when you’re refactoring code for unit tests. Here’s a simplified example from a bit of code that I refactored today.

module Ovs = struct

  let vsctl args = call_cmd "ovs-vsctl" args

  let create_bond name bridges interfaces props =
    let prop_args = make_bond_properties name props in
    let iface_args = do_stuff_with interfaces prop_args in
    let bridge_args = do_other_stuff_with bridges in
    vsctl [ bridge_args; iface_args ]

end

The above code is loosely based on xen-api’s network daemon, a service which configures an XCP host’s networking. The above module is for the Open vSwitch backend, which uses the ovs-vsctl command line tool to convigure the vSwitch controller. I just implemented some new functionality in the make_bond_properties function (not shown above), and I want to test that ovs-vsctl command is being invoked properly.

I want to be able to test that the list of arguments we’re passing to the vsctl function is correct, but this function calls vsctl directly with the arguements, so I can’t “see” them in my test case. We could just split create_bond into create_bond_arguments and do_create_bond functions. But if we ever write unit tests for the other 20 functions that call vsctl, we’ll have to do the exact same thing for all of them. Instead, we’ll refactor the Ovs module so that we can pass in an alternative implementation of vsctl.

module Ovs = struct
  module Cli = struct
    let vsctl args = call_cmd "ovs-vsctl" args
  end

  module type Cli_S = module type of Cli

  module Make(Cli : Cli_S) =

    (* continue with original definition of Ovs *)
    let create_bond name bridge interfaces props =
      let prop_args = make_bond_properties name props in
      let per_iface_args = do_stuff_with interfaces prop_args in
      vsctl [ bridge; per_iface_args ]

  end

  include Make(Cli)

end

You can see that we’ve just inserted a few lines of code around the original Ovs module implementation. We moved the vsctl function into Ovs.Cli, and we moved the rest of the definition of Ovs into Ovs.Make(Cli : Cli_S). The neat bit is at the end, when we include Make(Cli) inside of module Ovs. This calls the Make functor with a module that contains the original definition of the vsctl function, and includes that in the Ovs definition. Now we have a functorized module, which we can customize in our test cases. Yet we haven’t changed the definition of the original Ovs module at all, so we don’t have to change any of the code that depends on the Ovs module! This trick also works on toplevel modules in *.ml files, too.

Now our testcase is easy to write:

module TestCli = struct
  let vsctl args = assert_args_are_correct args
end

let test_create_bond =
  let module Ovs = Ovs.Make(TestCli) in
  let test_properties = ... in
  Ovs.create_bond "bond0" "xapi0" ["eth0"; "eth1"] test_properties

So the test is actually performed by the vsctl command, which we injected into the Ovs module using the Ovs.Make functor. Easy!

One more thought: I had considered using first class modules to inject test dependencies into a module like Ovs. This would have worked as well, but I couldn’t think of a good way to do it that wouldn’t have required rewriting all the Ovs call sites (I didn’t think to hard about it, so maybe it’s easier than I think). Also, first class modules are really meant for swapping dependencies at runtime, more akin to what Java programmers mean when they say “dependency injection.” In this case, we really would rather have the dependency injection done at compile time, so there’s no real benefit to using first class modules.

Advertisements

Written by mcclurmc

December 18, 2012 at 7:31 pm

Posted in OCaml, Unit testing