注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

神魔破杜梓的叨叨堂

Programming every day!

 
 
 

日志

 
 
 
 

Flex 测试驱动开发  

2008-06-12 17:11:57|  分类: My Tech |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
原文来自adobe devnet

Unit testing isn't a particularly common practice among Flash developers. Perhaps that's because so many of us are self-taught and not aware of what goes on in other languages, or perhaps it's because, until recently, many Flash-based projects were relatively small-scale.

In this article, I am going to deal with the how and what, but not necessarily the why for Test Driven Development for ActionScript 3.0 and Flex 2. If you want to know 'why', a simple Google search will return a plethora of resources.

I'll do my best to collate and share what I have discovered over this period and show you how to unit-test using FlexUnit, hopefully saving you from some of the frustration and head-scratching I experienced.

Requirements

Flex 2 SDK and Flex Builder

FlexUnit from Adobe Labs

flexunit .zip (ZIP, 1.8 MB)

Prerequisite knowledge

Object-oriented programming (OOP), and some ActionScript 2.0 (enough to understand ActionScript 3.0.)

What is Test Driven Development (TDD) and Unit Testing?

Test Driven Development is one of the rules of eXtreme Programming (XP).

Originally, XP programmers wrote tests for any part of their code that could break. However, this soon evolved in to the practice of writing the tests before writing the actual code. This may sound strange, but there is reason behind this madness; by knowing what can go wrong, you have a better idea of what your code needs to be able to handle.

TDD has evolved in to a process by which code is generated, as opposed to a testing process. It does still effectively perform tests, but that is more of a by-product.

The first thing you need is a Unit Testing framework. You can write your own, but that is not necessary. Most people use one of the existing frameworks such as FlexUnit or ASUnit.

In TDD, functionality is added in very small chunks: You come up with a test, you write the test, you write the code to satisfy the test, possibly refactor it to make it more efficient, and then start with the next test. Each test may only take a minute or so to write; it may take 10; one test should not take hours though.

This quote, from Dave Astels gave me my first clear definition of TDD:

"It's about figuring out what you are trying to do before you run off half-cocked to try to do it. You write a specification that nails down a small aspect of behaviour in a concise, unambiguous, and executable form. It's that simple. Does that mean you write tests? No. It means you write specifications of what your code will have to do. It means you specify the behaviour of your code ahead of time. But not far ahead of time. In fact, just before you write the code is best because that's when you have as much information at hand as you will up to that point. Like well done TDD, you work in tiny increments... specifying one small aspect of behaviour at a time, then implementing it." – Dave Astels

There is in fact a kind of "testing dance" you perform, and it's known as the Test-Code-Simplify cycle:

The Test-Code-Simplify cycle (Quoted verbatim from "Extreme Programming Applied", p159):

  1. Write a single test
  2. Compile it. It shouldn't compile, because you haven't written the implementation code it calls
  3. Implement just enough code to get the test to compile
  4. Run the test and see it fail
  5. Implement just enough code to get the test to pass
  6. Run the test and see it pass
  7. Refactor for clarity and "once and only once"
  8. Repeat

Understanding terminology

I'm going to use FlexUnit for this tutorial. The "Flex" part may be a little misleading. You do need Flex in order to use it, but the framework can be used to test pure ActionScript 3.0 files as well.

Before looking at how to use FlexUnit, let's get familiar with some terminology. Bear with me—it's a bit of a chicken and egg situation, and will make more sense as you construct tests later on.

assertions

An assertion is a statement of expected outcome. In other words, it is the expected result of your test. You may assert that the sky should be blue, or that you want x to be equal to y. In TDD if your assertion turns out to be something other than what you expected, you write code to correct that in your class.

test fixture

There may be some setup that is constant across all of your tests. For example, each test may depend on the creation of a certain object, property or process. A test fixture is useful if you have two or more tests for a common set of objects, as it avoids duplicating the code necessary to initialize and clean up the common objects. Methods called setUp() and tearDown() are provided for this common code, and any properties used can be declared at the top of the class.

test case

A test case is the smallest unit of testing. It checks for a specific response to a particular set of inputs. Your test class must extend the TestCase class and have a constructor that calls the super class's constructor. In some frameworks, all test methods should begin with the word "test." So if you are testing that the beer is cold, your method will be called something like testBeerIsCold().

test suite

As more and more unit tests accumulate for a given project, you will want a way to group them. Running them one at a time would be cumbersome. A TestSuite can contain other TestSuites, or individual tests, and is a way of declaring which test must be run in the same session, regardless of whether or not they are in the same file. This way of aggregating tests is very handy as you'll soon see.

test runner

A test runner is a component that often has a graphical interface and displays the outcome of our tests. In FlexUnit you tell your TestRunner which TestSuite you want to use, and then call its startTest() method.

test harness

The class that extends TestCase may contain a test suite, will contain test cases and may possibly contain test fixtures. From what I can gather this class is referred to as a test harness, although usage of this term seems to vary in the documents I've looked at. Some documentation considers that the harness is the process that instantiates the runner and passes in the suites that you want to run, while Wikipedia defines a test harness as the an Automated Test Framework (ATF).

Congratulations if you managed to get through all that. It wasn't exactly exciting reading, but they are useful terms to refer back to as you look at the examples below.

Let's get on with installing FlexUnit and creating our tests...

Installing FlexUnit

Installing FlexUnit is pretty straight forward. You get the Flex Unit source files, set up a Flex project, and in the process, you specify that the project should use the flexunit.swc file:

  1. Get the flexunit .zip archive (from the Requirements area) and extract it to your hard drive. I created a new folder on my C drive called FlexDev and extracted it there, so the path on my machine is C:\FlexDev. Once extracted, FlexDev will contain a folder called flexunit, which in turn contains further sub folders (bin, docs and src folders).
  2. Create the Flex Project. Open up FlexBuilder 2 and create a new Flex project using: File > New > Flex Project.
  3. Choose a Basic Service (in other words, not Data Services) and press Next.
  4. Give your project a name of 'TDD Example', deselect the Use default location option, and browse to the same location as before (on my machine this is the C:\FlexDev folder). This is the location where you are going to set up your project. Click Next. (If you pressed Finish by mistake, skip to the end of this list).
  5. Select the Library Path tab, click the Add SWC… button and browse to the SWC file, which is in the bin folder of flexunit (using my set-up, the path is C:\FlexDev\flexunit\bin\flexunit.swc).
  6. Press Finish button and you're done.

Note: If you need to add the SWC file to an existing project, right-click your project folder, select properties from the pop-up list and continue from step 5 above.

Am I done?

You're almost there. First I am going to briefly describe the three files that you are about to create:

  • The first file is the .mxml, (which is set as the default application for our Flex project). This file contains an instance of the TestRunner component (the visual component that displays the test results). The file specifies to the component which test suites you want to run, and then starts the test. I simply called this "main.mxml."
  • I'll refer to the second file as the test class. It is the one that contains the "test runner." Some developers refer to this as the test harness and some would simply call it a test runner. In this file, decide what you need to assert, and then write your test cases in it. I've named this class AccountTest.as as the example in this tutorial relates to money.
  • Finally there is the actual class, Account.as. Build this file up bit by bit as you create more and more tests.

Creating the three files

Creating main.mxml:

Here is the MXML that belongs in the main.mxml file. Open up the file and copy and paste this in:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:flexunit="flexunit.flexui.*"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[

private function onCreationComplete():void
{
testRunner.test = AccountTest.suite();
testRunner.startTest();
}
]]>
</mx:Script>
<flexunit:TestRunnerBase id="testRunner" width="100%" height="100%" />
</mx:Application>

There are a few things to notice:

  1. We've defined a new xml namespace in the Application tag
  2. We have a creationComplete even calling our onCreationComplete method
  3. we created an instance of TestRunnerBase

We defined a new xml namespace in the Application tag:

xmlns:flexunit="flexunit.flexui.*"

If you have created your own components in Flex before, this will be familiar to you. If not then I will briefly explain.

This creates a namespace called "flexunit," which refers to components located in the flexunit.flexui package. We have access to this package of course, because you associated the flexunit.swc file with the project library earlier.

To instantiate an instance of the component in MXML, you can use code similar to the following:

<flexunit:TestRunnerBase id="testRunner" />

If you had declared your namespace as "bananas", you would instantiate your component using code similar to:

<bananas:TestRunnerBase id="testRunner" />

But you've used common sense and gone for a more descriptive name.

The TestRunnerBase component is TestRunner (see the "Understanding Terminology" section, above, for a refresher). I assigned it an id of testRunner.

In the Application tag you also have the creationComplete event handler, which calls onCreationComplete(). This method sets the test suite(s) you are going to use, and then calls the startTest() method of the TestRunner to put everything in to action.

Finally, underneath the script block you instantiate our TestRunner as previously described, with a width and height of 100%.

Okay, that's it. You'll be glad to hear that you don't really need to touch this file again in this tutorial.

Creating AccountTest.as:

Create a new ActionScript file in the main project folder (at the same level as main.xml) called AccountTest.as, and paste the following code into it:

package
{
import flexunit.framework.TestCase;
import flexunit.framework.TestSuite;

public class AccountTest extends TestCase
{
public function AccountTest(methodName : String){
super(methodName);
}

public static function suite():TestSuite{
var accountTS:TestSuite = new TestSuite();
//tests are added to the suite here
return accountTS;

}
}
}

There are a few things to notice:

  1. The class extends TestCase.
  2. There is one static function called suite (which returns a TestSuite).
  3. The constructor calls its super class, passing it some method name.

Creating Account.as

Finally, you must create the Account class. Create a new ActionScript file in the main project folder (at the same level as main.xml and AccountTest.as) called Account.as and paste the following code into it:

package
{
public class Account
{
public function Account(){

}
}
}

Not much to say about this. It is the bare bones of the actual class.


Writing the first test

I'm going to plagiarize (with permission) the example used by Simon Whacker to demonstrate as2lib's unit testing, because it was particularly clear and easy to understand.

We are going to create an application for a bank, and our first requirement is to allow the creation of a new account.

The first question to ask yourself is: When creating a new account, what should happen? Well, not much. Despite my best wishes, whenever I open a new account, it always starts off empty. How do I know that for sure? Well. I know because I can check my balance. So, when I open a new account and check my balance I should have ?0.00 in there. Okay, we have our first test.

Remember, in this tutorial, we write our test before we write the code in our class, and this is how it works:

In AccountTest.as, underneath our static "suite" method, paste in this code:

public function testNew():void{
var account:Account = new Account();
assertEquals("Expecting zero account balance", 0, account.getBalance());
}

Note: In some frameworks all tests must start with the prefix "test." With FlexUnit, this rule doesn't seem to be enforced, but for consistency I recommend doing so anyway. One reason to require any test method to start with "test" is that you can then shortcut the suite creation process. Simply introspect a class and any function that starts with "test" is considered a test.

In this case, you are testing the creation of a new account and so the method is called testNew.

As you can see, you create a new instance of the Account class, and then use an assertion method called assertEquals.

There are various types of assertion, but assertEquals basically checks that the two values passed in are ... [drum roll] ... equal. The eagle eyed among you may have noticed I am actually passing in three values:

  1. The first value is optional. It is a String and it should clarify the outcome you are looking to satisfy. In this case: "Expecting zero account balance."
  2. The next value is 0.
  3. The last value is a call to an, as yet non-existent, method of our Account class called getBalance(). I will write this in a moment.

Overall, this assert specifies that you want the value returned from getBalance() to equal zero.

Note: TestCase extends Assert, so there is no need to import the Assert class in order to use these assertion methods.

All you have to do now is add your test to the test suite.

Amend the static suite function in the AccountTest class to look like this:

public static function suite():TestSuite{
var accountTS:TestSuite = new TestSuite();
accountTS.addTest(new AccountTest("testNew"));
return accountTS;
}

You can see from the above code that I use the addTest method to add the test to the TestSuite. The object passed in is actually an instance of AccountTest (in other words, the class you are in). The name of the test to run, gets passed in as a String...

If you remember, the constructor for AccountTest looks like the following code:

public function AccountTest(methodName : String){
super(methodName);
}

It accepts the methodName of the test, and passes it on to its super class.

Note: This is the way I first learned to test, but you don't have to do it like this. The framework will automatically find all "test" methods in a TestCase if you pass the class reference to the constructor of a TestSuite.

Test It!

Okay, the setup is done. It's time to test.

If you think back to the Test-Code-Simplify cycle of eXtreme Programming that we looked at earlier. The first couple of stages were:

  • Write a single test.
  • Compile it. It shouldn't compile, because you haven't written the implementation code it calls

You've written a test, so try and compile the project.

The Errors window

Figure 1. The Errors window

The Flex Builder Problems pane should also be reporting the following error:

"Call to a possibly undefined method getBalance through a reference with static type Account."

Great! That's what we want. You haven't written getBalance() yet And so you should quite rightly have a compiler error.

Note: If you actually choose to ignore the error and run the application anyway, you will see the TestRunner, and the test will actually pass! I didn't expect this to happen so be aware of it. It is fine to ignore Flex Builder warnings (in fact, the kind of tests you'll do will probably cause a few warnings) but don't ignore compile-time errors.

From further research, I found out that the test passes based on the last time you saved. So if you saved an empty test method, it would work; or, if you saved the empty testcase it would run and report no failures. When there are compiler errors, a new SWF file doesn't replace the old SWF, so you are in fact running the old SWF file.

Now take a look at the next two steps in the Test-Code-Simplify cycle:

  • Implement just enough code to get the test to compile.
  • Run the test and see it fail.

Amend the Account class to look like this (additions are highlighted in yellow):

package
{

public class Account
{
private var _balance:Number;
public function Account(){

}

public function getBalance():Number{
return _balance;
}
}
}

You now added your getBalance() method, which returns _balance, and you have also declared this variable called _balance (intended to hold our account balance amount of course), but _balance hasn't been initialized with a value yet. Compile, and you should see something like the following:

The TestRunner GUI

Figure 2. The TestRunner GUI

This error is a good thing! Here we can see the test that failed (testNew) and why it failed. The big red bar would be green if the test had passed.

Now you know your TestRunner is working properly. You know the test should have failed, you know your test did fail, and you know why it failed.

The TestRunner tells us that it expected a value of zero but received a value of NaN (Not a Number) which is what ActionScript 3.0 returns (variables typed as Number and not assigned a value are returned as NaN in ActionScript 3.0).

Continuing on, the next two steps in the Test-Code-Simplify cycle are:

  • Implement just enough code to get the test to pass
  • Run the test and see it pass

All you need to do now is give _balance an initial value of zero. Change your Account class's constructor to look like the following:

public function Account(){
private var _balance:Number = 0;
}

Now run the test:

The test succeeds

Figure 3. The test succeeds in the All Tests tab

Bingo. The bar is green. Here we are viewing the "All Tests" tab in the left pane, which shows that the testNew test has passed. You know it passed for the right reasons too, because you saw it fail when conditions were wrong.

The final two points in the Test-Code-Simplify cycle are:

  • Refactor for clarity and "once and only once"
  • Repeat

There's little need to refactor here, and so on with the next test.

Onwards and upwards

You've completed your first test—the hard part is over with. Take a break if you're exhausted because in order to see the real value of TDD we are going to implement a couple of extra tests. Most of the following code will be copy and paste with little need for additional explanation, so it shouldn't take long to get to the end now

Give me some credit

In many ways this next part of the tutorial is the most important. I hope to demonstrate how TDD can help you spot mistakes and prevent you from making changes which could break existing code and leave you unaware. Catching these problems when they occur, can save hours of debugging and headache further down the line.

Let's say your bank customers can open an account and now they need a way to credit their accounts. I'm sure you can think of plenty of things you need to test for here. Just as an example:

  • crediting an account with a null amount (avoid this!)
  • crediting an account with a real value (must work!)
  • crediting an account with a negative amount (that's a debit not a credit)
  • an account credit which includes fractions of a penny (how to handle?)

We're not going to implement all of these, but we'll use a couple of them to highlight some points.

Credit with null value

This seems pretty straight forward so first let's write the test:

public function testCreditWithNullValue():void{
var account:Account = new Account();
account.credit(null);
assertEquals("Expecting zero account balance", 0, account.getBalance());
}

Again, you have called a method that doesn't exist yet credit(), but in doing so you have effectively specified that it is required, and must be capable of accepting a value. This shows how TDD helps you write your specifications.

Now add your test to our test suite:

accountTS.addTest(new AccountTest("testCreditWithNullValue"));

Check that your application doesn't compile; sure enough, Flex Builder gives you a compile error because you haven't written the credit() method yet.

Next, write just enough code in the Account class to let it compile.

public function credit(amount:Number):void{
_balance += amount;
}

Run the test and see it fail:

Only, this one doesn't fail.

Flex Builder warns is that AccountTest ('null' is being used where 'Number' is expected), but there is no error. In fact ActionScript 3.0 appears to handle the attempt at passing in a null and returns a balance of zero.

In ActionScript 2.0, adding null to zero would equal NaN (Not a Number), so let's quickly check that our assumption about this is correct by making the test fail on purpose.

Change the code as follows:

account.credit(null + 10);

When we run the test it does indeed fail, because it is expecting zero, but receives 10. It looks like the code is doing what you wanted, so remove the "+ 10" and write the next test.

Credit with real value

Now make sure you can credit the account with a real value.

Use the amount ?12.34, although the currency is obviously irrelevant here.

Once again, create a test and add it to the test suite. I show the test as follows:

public function testCreditWithRealValue():void{
var account:Account = new Account();
account.credit(12.34);
assertEquals("Expecting account balance with pounds and pence.", 12.34, account.getBalance());
}

Again, you create a new instance of the Account class, call the credit method (which now does exist) and pass in a value of 12.34. Therefore, you expect a balance of 12.34 returned.

Finally, this test passes for the first time. With something as simple as this (considering our other tests are also in place) I don't think you need to double check it by making it fail. On with the final test.

Credit to three decimal places (fractions of a penny)

In the next scenario, the bank has specified that it will allow account credits which include fractions of a penny, but it will always round these amounts down and keep the extra (they are a bank after all); so if you put in thirty seven and a half pence, the account would be credited with 37 pence. Here, you simply check a number to three decimal places.

Write the test (see below) and add it to the test suite (not shown):

public function testCreditWithRealValueTo3DecimalPlaces():void{
var account:Account = new Account();
account.credit(1.234);
assertEquals("Expecting account balance of 1.23", 1.23, account.getBalance());
}

Run the application and see it fail. Good. There is enough code in place for the application to compile already, but of course you are expecting a rounded-down figure, and the current credit method is not set up for that, so you get the following TestRunner error:

Error: Expecting account balance of 1.23 - expected:<1.23> but was:<1.234>

That's exactly what is expected. Now you write enough code to make it pass, which means changing the credit() method to round off the amount. Change it to look like the following (changes in highlight):

public function credit(amount:Number):void{
var rounded = Math.floor(amount * 100)/100;
_balance += rounded;
}

If you run the test, it passes. Notice that Flex Builder warns you that you forgot to give the rounded variable a Type definition. To do that, change the code to:

var rounded:int = Math.floor(amount * 100)/100;

Run the application and...

Hang on a minute. We seem to have broken a couple of tests.

Let's look at the first one:

Running the testCreditWithRealValue test

Figure 4. Running the testCreditWithRealValue test

The testCreditWithRealValue was passing before, so what did we do to break it? Well as you can see, the test was expecting a real value of 12.34 but it only received 12.

In a moment of distraction, we typed the variable incorrectly as int, and of course integers are whole numbers. In this case my contrived example was pretty obvious, but it shows how changing or implementing new functionality can break earlier code, and how TDD alerts you to that fact instantly.

All you need to do to fix this particular error is to type our variable as Number:

rounded:Number = Math.floor(amount * 100)/100;

Now run the application and all tests pass.

Let's look at the testCreditWithRealValue test again for a moment... luckily you chose to use decimals in the "real value" test. Obviously an integer is a valid "real value" too, and if you had just used an integer, you wouldn't have spotted this error so easily. It is important to spend a moment naming our tests accurately, and more importantly, ensuring that you can't break a test down further in to smaller tests. One of the XP maxims is "Test until fear turns to boredom". TDD is great, but it's not an instant solution to all your problems. I wanted to show both how useful it can be, and how thin the distinction can be between useful feedback and no feedback at all.

There is so much left to cover here. I haven't touched upon collaborators, mock objects, test fixtures, organizing tests and test hierarchy, and so forth. What I have tried to do, is cover enough ground to get you started. If there is enough interest (and when I am more familiar with the ins and outs of TDD myself) I might write a follow up tutorial, but for now, happy testing. You can contact me at neil @ nwebb dot co dot uk.



  评论这张
 
阅读(477)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017