1st project setup with unit testing

Ramiro Araujo's Avatar

Ramiro Araujo

06 Mar, 2012 02:19 PM

Hi all
This is my first post in here, and I'm just starting to dive into RL (after reading the book :D), and also wan't to start with unit testing, which I've never did before in flash/flex apps.
I've searched but I couldn't find any post about project setup with unit testing. I want to start with a new project and have the unit testing framework up and running right from the start. How would you do that setup? What unit testing framework would you recommend? should I have a different project or module (in the IDE) for the testing? Should I add a mocking library right from the start?

Any prescriptive recommendation or example is welcome! I'll have time later to choose my own ways :)

Thanks!
Ramiro

  1. Support Staff 1 Posted by Shaun Smith on 07 Mar, 2012 09:31 AM

    Shaun Smith's Avatar

    Hi Ramiro,

    I use the built-in unit testing capabilities of Flash Builder (Flex Unit). I keep the tests in the same project as the source, so an example project might look like:

    src
      com/domain/module/impl/Greeter.as
    test
      com/domain/module/impl/GreeterTest.as
    

    test is a top-level folder that you add to the source path (Flex Build Path -> Source Path -> Add folder).

    Once you've created the blank SomeClass.as file you can right-click it in the package explorer and select New -> Test Case Class (or just hit CMD-N and start typing test, and arrow down to Test Case Class), which opens a wizard to create a new test case for the selected class.

    With the wizard open, change the source folder to the test folder by hitting browse up at the top (it defaults to the src folder which is really annoying), and hit enter. This should create a new test case for the currently selected class.

    With the new test case open hit CMD-3 and start typing test. Arrow down to Execute Editor Flex Unit Test (the menu will remember your previous choices and becomes more convenient to use the more often you use it) and hit enter. This should run the currently focused test through the Flex Unit IDE runner. The test case should fail because Flex Unit won't find any tests in it yet.

    Add a simple test (do this before adding any code to the Greeter implementation):

    [Test]
    public function greeter_says_hello_to_Jack():void
    {
      const greeter:Greeter = new Greeter();
      assertThat(greeter.greet("Jack"), equalTo("Hello Jack"));
    }
    

    You should get a compilation error as the Greeter won't have the greet() method yet. So add the absolute minimum to Greeter to get compilation working again:

    public function greet(name:String):String
    {
      return "";
    }
    

    Jump back to the test case and hit CMD-3, type test, select Execute Editor Flex Unit Test and hit enter. The test should run but fail. It's important to always start with a failing test and only add the absolute minimum implementation code to get the current test to pass.

    Also, before you run any test you should always predict the outcome of the test. This helps prevent "lazy" coding where you just type stuff, execute it and see what happens at runtime. So, before running the test at this point you should predict that "The test should fail". Run it and make sure it does. When you can't easily guess the outcome of a test it usually means that you've written too much code in one go, you aren't concentrating, or you're not sure what you're doing.

    Next we make the test pass. But at this point, we do the simplest thing possible to make it pass (even if that means that we cheat). It's important not to get caught up in details at this stage. Just make the test pass. Red to green:

    public function greet(name:String):String
    {
      return "Hello Jack";
    }
    

    Notice that we hard code the exact response we are expecting in the test. This is the simplest thing we can do to make the test pass. Before running the test, predict the outcome again. "This will pass". Make sure it does.

    If all goes well we should have a passing test. Resist the temptation to tidy up or fix the Greeter implementation - we must only ever write code to make a test pass, and at this point we only have one test, and it passes.

    It's time to write another test. We know that we can write a failing test by passing in a different name to the greeter, so lets do that:

    [Test]
    public function greeter_says_hello_to_Jill():void
    {
      const greeter:Greeter = new Greeter();
      assertThat(greeter.greet("Jill"), equalTo("Hello Jill"));
    }
    

    Again, before running the test predict the outcome. It should fail. Jump to the implementation and do the simplest thing possible:

    public function greet(name:String):String
    {
      return "Hello" + name;
    }
    

    Predict the outcome. "This should pass". Run the test. Boom, it fails, we forgot a space. Fix the code:

    public function greet(name:String):String
    {
      return "Hello " + name;
    }
    

    Predict the outcome. "This should pass". Run the test. Hopefully it does!

    If all is well, it is time to pick the next simplest business requirement. Our greeter should not say hello if the name is blank. Add a new test:

    [Test]
    public function greeter_ingores_blank_name():void
    {
      const greeter:Greeter = new Greeter();
      assertThat(greeter.greet(""), equalTo(""));
    }
    

    Predict the outcome. "This should fail". Run the test, see it fail, and jump back to the implementation.

    public function greet(name:String):String
    {
      if (name == "") return "";
      return "Hello " + name;
    }
    

    Predict the outcome. "This should pass". Run the test. It should pass.

    At this point everything should be passing, but we might decide there is too much duplication in the test, so we tidy things up a bit. We pull the greeter instantiation out of our tests and into a setup method:

    private var greeter:Greeter;
    
    [Before]
    public function before():void
    {
      greeter = new Greeter();
    }
    

    Our tests should be a little cleaner now:

    [Test]
    public function greeter_says_hello_to_Jack():void
    {
      assertThat(greeter.greet("Jack"), equalTo("Hello Jack"));
    }
    
    [Test]
    public function greeter_says_hello_to_Jill():void
    {
      assertThat(greeter.greet("Jill"), equalTo("Hello Jill"));
    }
    
    [Test]
    public function greeter_ingores_blank_name():void
    {
      assertThat(greeter.greet(""), equalTo(""));
    }
    

    But ignoring blank names feels a bit wrong. We decide that the greeter should throw an error if the name is blank, so we change that test as follows:

    [Test(expects="Error")]
    public function greeter_throws_on_blank_name():void
    {
      greeter.greet("");
    }
    

    This tells FlexUnit that we expect an error to be thrown and that the test should fail otherwise.

    Again, before running the test predict the outcome. It should fail. Jump to the implementation and do the simplest thing possible:

    public function greet(name:String):String
    {
      if (name == "") throw new Error("Invalid name");
      return "Hello " + name;
    }
    

    Predict the outcome. If all is well it's time to pick the next feature. Or, perhaps we decide that the greeter should throw an error not only when the name is blank, but also when it is null:

    [Test(expects="Error")]
    public function greeter_throws_on_null_name():void
    {
      greeter.greet(null);
    }
    

    Predict the outcome. Ok, I'll stop saying that from now on, you get the idea! So, we do the simplest thing possible (even though it's messy):

    public function greet(name:String):String
    {
      if (name == "") throw new Error("Invalid name");
      if (name == null) throw new Error("Invalid name");
      return "Hello " + name;
    }
    

    We just need to get the test to pass, we don't care about the implementation. Once the test passes we can confidently refactor. So, with a handful of passing tests in place, we decide to refactor the greeter:

    public function greet(name:String):String
    {
      validate(name);
      return "Hello " + name;
    }
    
    private function validate(name:String):void
    {
      if (!name)
        throw new Error("Invalid name");
    }
    

    Predict the outcome (ha, I said it again!). And that's the basic flow:

    • Write a failing test
    • Predict the outcome
    • Write the simplest implementation possible
    • Predict the outcome
    • Repeat
    • Refactor

    I would get comfortable with this flow (it should start to feel natural eventually) before getting into Mocking.

    Also, be sure to have a look at the test setup in Robotlegs:

    https://github.com/robotlegs/robotlegs-framework

    Hope that helps!

  2. Support Staff 2 Posted by Stray on 07 Mar, 2012 02:42 PM

    Stray's Avatar

    This is such a great response, Shaun.

    The only thing I would add / emphasise is that when you "Predict the outcome" - don't just predict pass / fail - if you're predicting a fail, predict the nature of the fail.

    "The array length will be 0, when we expect 1" for example.

    Why? Because this keeps you reading the fail messages - if you predict fail with length 0 and the fail is actually 'the length was 2' or 'the array was null' then this tells you something extra.

    It's important that tests don't just fail, but fail in the exact way that we expected them to. I'm amazed by how frequently my test fails differently from how I expected (so I revise the test, or break it into smaller chunks).

    Just my tiny addition to a super-awesome post!

    Stray

  3. 3 Posted by rama.araujo on 07 Mar, 2012 08:45 PM

    rama.araujo's Avatar

    Thanks a lot Shaun!
    Even if it's not exactly what I mean in my post (I expected a few simple recommendations, libraries, configurations, etc) this was really MUCH enlightening. It shows me the type of focus you should have when doing TDD (if I understood correctly, this is a TDD approach right?, even though I was asking for a simple unit testing config setup :D:D)
    Again, I really really appreciate it!

    One thing I forgot to mention is that I plan to use pure AS3 projects, and I use IDEA. I can find the IDE setup myself, but can I use Flex Unit without Flex framework or should I use ASUnit?
    I asume the API might be different but the idea behind is the same right?
    If I should use ASUnit, the [Test] metadata works? or it has a specific api for asserting errors or exceptions?

    Thanks!
    Ramiro

  4. 4 Posted by Stray on 07 Mar, 2012 08:51 PM

    Stray's Avatar

    FlexUnit works fine with or without the flex framework - Robotlegs itself is pure AS3.

    hth

    Stray

  5. Support Staff 5 Posted by Shaun Smith on 07 Mar, 2012 08:57 PM

    Shaun Smith's Avatar

    hehe, yeh I got carried away. As Stray mentions, FlexUnit works just fine with plain AS3 - and I'm pretty sure that IDEA has FlexUnit integration out-of-the-box.

  6. 6 Posted by rama.araujo on 07 Mar, 2012 09:21 PM

    rama.araujo's Avatar

    Great!
    Well, hands on then.

    BTW, I created this thread without being registered (but using my registered email), so I can't close it or whatever. Can some admin mark it as "resolved"?

    Cheers
    Ramiro

  7. Ondina D.F. closed this discussion on 07 Mar, 2012 09:25 PM.

Comments are currently closed for this discussion. You can start a new one.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac