Agile software development using Kanban & Scrum. We code Flex & Ruby on Rails in Auckland, New Zealand.

  • RSpec Style Unit Testing with FlexUnit4

    We’re still early into our TDD for Flex program, and I am trying to learn “how” to write useful tests while not getting bogged down in any extremes, at least not until I understand it better.

    I asked our CTO who is an accomplished ruby developer to guide and mentor me a little. He prefers to use RSpec for Ruby development. RSpec is designed for “Behaviour Driven Development” which Dan North introduces in a compelling way here. However it’s not clear at first glance why RSpec fits with Dan’s “Acceptance Criteria” template. (Cucumber)[http://cukes.info/] on the other hand, fits really well, but there is nothing even close to Cucumber for Flex that I can see.

    Meanwhile, I needed something applicable and that guided me into my first tests, so I tried to adopt the DSL that RSpec uses. I think I’ve done an adequate job and I found it really helpful and inspiring. This article outlines what I do.

    I am using real-world code throughout these samples. Mockolate is my mocking framework of choice here because it just works with FlexUnit4. Mockito also looks great but it’s integration with FlexUnit isn’t quite what I’d hope for.

    Create a Case (Context)

    The test case to me, represents the context of “how the unit (class) under test will be configured for testing”. There are sub-contexts which I’ll get to in a moment.

    public class ProxyListCase {
    
      private var proxyList:ProxyList;
    
      private var configurator:IProxyConfigurator;
    
      [Before(order=1,async, timeout=5000)]
      public function prepareMockolates():void {  
            Async.proceedOnEvent(this, prepare(IProxyConfigurator,VOProxy), Event.COMPLETE);
      }
    
      [Before(order=2)]
      public function setup():void {
        proxyList = new ProxyList();
    
        configurator = nice(IProxyConfigurator);
        proxyList.proxyConfigurator = configurator;
      }
    
    }
    

    In the first “Before” section I pre-initialise Mockolate. In the second, I set up the context.

    The context for ProxyList is:

    • That we have a ProxyList instance to test
    • That ProxyList is configured with required injections for testing anything, in this case, configurator is required.

    Stubbing the Test

    My first test, if I were to write it down, is:

    given
      a simple VO
      a Proxy
    then
      it builds a proxy on add
    

    Which I deftly code as:

      [Test(given="a simple VO and a Proxy",
        it="builds a proxy on add")]
      public function ItBuildsAProxyOnAdd():void {
        fail();
      }
    

    The metadata for the Test doesn’t “mean” anything to FlexUnit. However it can be pulled out by a custom runner on your CI server if you so wish. What it does tho, is gives you, and the next person to read the test, a very clear understanding of the test’s intent. It also lets you mark your sub-contexts in given. Let me show you how.

    Sub-contexts (Givens)

    First I translate the givens into with<HelperMethods>

      [Test(given="a simple VO and a Proxy",
        it="builds a proxy on add")]
      public function ItBuildsAProxyOnAdd():void {
        withSimpleVO();
        withProxy();
    
        fail();
      }
    

    I then implement these new ‘givens’

      public function withProxy():void {
        proxy = nice(VOProxy);
        stub(factory).method("newInstance").returns(proxy);
      }
    
      private function withSimpleVO():void {
        newVO = {name: "hi"};
      }
    

    These given’s get re-used a lot. As you refactor your tests you get a much clearer idea of what your assumptions about prerequisites are.

    Do something and make assertions

    Now for the simple bit, the test. This should read very clearly from the it metadata. In this case, remember: “it builds a proxy on add”

      [Test(given="a simple VO and a Proxy",
        it="builds a proxy on add")]
      public function ItBuildsAProxyOnAdd():void {
        withSimpleVO();
        withProxy();
    
        proxyList.add(newVO);
    
        verify(factory).method("newInstance").once();
      }
    

    So we added using a prerequisite newVO and then we verified that the factory was asked to build a proxy.

    I don’t think I can make a test clearer than that. Any thoughts?

    Heres the whole class with a few tests, it’s a little different but you get the idea.

    public class ProxyListCase {
      private static const PROXY_DISPOSE:String = "dispose";
      private static const CONFIGURATOR_BUILD:String = "configure";
    
      private var proxy:VOProxy;
      private var proxyList:ProxyList;
      private var configurator:IProxyConfigurator;
      private var newVO:Object;
    
      [Before(order=1,async, timeout=5000)]
      public function prepareMockolates():void {  
            Async.proceedOnEvent(this, prepare(IProxyConfigurator,VOProxy), Event.COMPLETE);
      }
    
    
      [Before(order=2)]
      public function setup():void {
        proxyList = new ProxyList();
    
        configurator = nice(IProxyConfigurator);
    
        proxyList.proxyConfigurator = configurator;
      }
    
    
      [Test(given="a simple VO and a Proxy",
        it="builds a proxy on add")]
      public function ItBuildsAProxyOnAdd():void {
        withSimpleVO();
        withProxy();
    
        proxyList.add(newVO);
    
        verify(configurator).method(CONFIGURATOR_BUILD).once();
      }
    
    
      [Test(given="a Proxy, and one VO added",
        it="disposes the proxy on remove")]
      public function ItDisposesTheProxyOnRemove():void {
        withSimpleVO();
        withProxy();
        withOneVOAdded();
    
        proxyList.remove(newVO);
    
        verify(proxy).method(PROXY_DISPOSE).once();
      }
    
      [Test(given="a Proxy, and one VO added-removed-readded",
        it="builds a proxy on re-add")]
      public function ItBuildsAProxyOnReadd():void {
        withSimpleVO();
        withProxy();
        withOneVOReAdded();
    
        verify(configurator).method(CONFIGURATOR_BUILD).twice();
      }
    
    
      [Test(given="a Proxy and an added VO",
        it="disposes the proxy on dispose")]
      public function ItDisposesTheProxyOnReset():void {
        withSimpleVO();
        withProxy();
        withOneVOAdded();
    
        proxyList.dispose();
    
        verify(proxy).method(PROXY_DISPOSE).once();
      }
    
    
      /* Sub Contexts */
    
      private function withSimpleVO():void {
        newVO = {name: "hi"};
      }
    
      public function withProxy():void {
        proxy = nice(VOProxy);
        stub(configurator).method(CONFIGURATOR_BUILD).returns(proxy);
      }
    
      private function withOneVOAdded():void {
        proxyList.add(newVO);
      }
    
      private function withOneVOReAdded():void {
        proxyList.add(newVO);
        proxyList.remove(newVO);
        proxyList.add(newVO);
      }
    
    }