测试驱动开发案例之自动售货机(第1集)

测试驱动开发(TDD, Test Driven Development)是一种很有意思的软件开发方式,本集以较小的步伐体验TDD。

/**  A program simulation of an automat.
 *
 *  Director: Zuo Baoquan
 *  Contact me:
 *     Blog:   http://www.blogjava.net/BaoquanInside
 *     MSN: Baoquan.Zuo [at] hotmail.com
 *     Email:  Baoquan.Zuo [at] gmail.com
 *
 *  Copyright 2006 BEC Studio
 */
 
引言:
(该问题截取自《面向对象的设计与模式》(Written by Cay Horstmann) Exercise 2.18
 
设计并实现一个模拟自动售货机的程序。通过给机器投入正确的硬币,便可以购买到相应的商品。用户从一个可用的商品列表中选择商品、投入硬币并得到商品。如果没有足够的钱币或由于此商品已销售完毕,则将硬币退回。操作员可以重新备并将钱币取走。
 
 
场景#1
地点:同济大学西南楼
涉众:我,空自动售货机
 
寝室楼里面刚搬来了一台空的自动售货机,那我们来测试一下:
 
public class TestEmptyAutomat extends TestCase
{
  public void testIsEmpty()
  {
    Automat automat = new Automat(); 
    assertTrue("it should be empty.", automat.isEmpty());
  }
}
 
Eclipse提示"Automat cannot be resolved to a type",好,看我的:
public class Automat
{
  public Automat()  // constructor stub
  {
  }
}
 
再露一手:
//class Automat
public boolean isEmpty()
{
  return true;
}
 
全部保存,运行测试,呵呵,绿色进度条!测试成功!
 
好,既然我们还没有投过硬币,那么余额应该是0了~
 
// class TestEmptyAutomat
public void testBalance()
{
  Automat automat = new Automat();
  assertEquals("the balance should be 0.", 0, automat.balance); 
}
 
继续使用我们的法宝:
// class Automat
public final int balance = 0;
 
运行测试,yeah!
 
看了一遍测试程序,决定优化一下:
public class TestEmptyAutomat extends TestCase
{
  protected void setUp() throws Exception
  {
    super.setUp();
    automat = new Automat();
  }
 
  public void testIsEmpty()
  {
    assertTrue("it should be empty.", automat.isEmpty());
  }
 
  public void testBalance()
  {
    assertEquals("the balance should be 0.", 0, automat.balance); 
  }
 
  private Automat automat;
}
再运行一次测试,呵呵,又是绿色!
 
场景#2
地点:同济大学西南楼
涉众:我,自动售货机(有一瓶百事可乐)
 
好消息,楼长在自动售货机里面放了一瓶Pepsi。
 
public class TestAutomatWithOnePepsi extends TestCase
{
  protected void setUp() throws Exception
  {
    super.setUp();
    automat = new Automat();
    pepsi = new Pepsi();  // 一瓶百事可乐
    automat.add(pepsi);  // 楼长阿姨放的~
  }
  
  public void testEmpty()
  {
    assertFalse("it should not be empty.", automat.isEmpty());
  }
 
  public Automat automat;
  public Pepsi pepsi;
}

接着创建Pepsi类
 
public class Pepsi
{
  public Pepsi() // constructor stub
  {
   
  }
}
 
再给Automat添加add方法:
// class Automat
public void add(Pepsi pepsi)
{
 
}
 
好,现在有两个TC(Test Case)了,为了运行两个测试案例,我们来创建一个Test Suite:
public class AutomatTests
{
  public static Test suite()
  {
    TestSuite suite = new TestSuite("Test for net.mybec.automat");
    //$JUnit-BEGIN$
    suite.addTestSuite(TestAutomatWithOnePepsi.class);
    suite.addTestSuite(TestEmptyAutomat.class);
    //$JUnit-END$
   
    return suite;
  }
}
 
编译成功,运行AutomatTests,红色进度条。TestEmptyAutomat绿色,TestAutomatWithOnePepsi红色。呵呵,看来要让add做点事情了。
 
// class Automat
public void add(Pepsi pepsi)
{
  goods.add(pepsi);
}
 
// 添加一个装Pepsi的数组列表
private final ArrayList<Pepsi> goods = new ArrayList<Pepsi>();

 
// 修改isEmpty方法
public boolean isEmpty()
{
  return goods.isEmpty();
}

 
再次运行AutomatTests,呵呵,绿色!我们喜欢!
 
好,我们再看看Automat的余额:
 
// class TestAutomatWithOnePepsi
public void testBalance()
{
  assertEquals("the balance should be 0.", 0, automat.balance);
}
 
运行一遍测试,Ok。
 
 
我们还没有投硬币,当然不能买百事可乐了:
 
// class TestAutomatWithOnePepsi
public void testCanBuyWithoutBalance()
{
  assertFalse("we cannot buy pepsi without money.", automat.canBuy(pepsi));
}
// class Automat
public boolean canBuy(Pepsi pepsi)
{
  return false;
}
 
我们太喜欢运行测试了,于是又忍不住运行了所有的自动测试(呵呵,实际上我们只需要点击一个运行按钮)。又是绿色~
 
好,如果Pepsi的价格是2元,我们投1块钱试试~
 
// class Pepsi
public static final int PRICE = 2;
 
// class TestAutomatWithOnePepsi
public void testCanBuyWithOneYuan()
{
  automat.put(1);
  assertFalse("we cannot buy pepsi with one yuan.", automat.canBuy(pepsi));
}
 
// class Automat
public void put(int yuan)
{
 
}
 
运行测试,绿色。显然1块钱买不到百事可乐。那就投2块钱吧:
 
// class TestAutomatWithOnePepsi
public void testCanBuyWithTwoYuan()
{
  automat.put(2);
  assertTrue("we can not buy pepsi with two yuan.", automat.canBuy(pepsi));
}
 
运行测试,红色进度条,JUnit提示“we can not buy pepsi with two yuan.” 天啊,这不公平。
想起来了,Automat.put什么也没做。于是我们添了几笔:
 
// class Automat
public void put(int yuan)
{
  balance += yuan;
}
 
public boolean canBuy(Pepsi pepsi)
{
  return balance >= Pepsi.PRICE;
}
 
public int balance = 0;  // 去掉了final
 
迫不及待地点击了运行按钮,yeah!终于能买到喜欢的Pepsi了(因为看到了绿色进度条~)。
 
于是急忙买了一瓶Pepsi:
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithTwoYuan()
{
  automat.put(2);
  automat.buy(pepsi);
  assertTrue("the automat should be empty.", automat.isEmpty());
}
// class Automat
public void buy(Pepsi pepsi)
{
 
}
 
Run Tests, Failed. So,
// class Automat
public void buy(Pepsi pepsi)
{
  goods.remove(pepsi);
}
Run Tests again, OK.
 
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithTwoYuan()
{
  automat.put(2);
  automat.buy(pepsi);
  assertTrue("the automat should be empty.", automat.isEmpty());
  assertEquals("the balance should be 0.", 0, automat.balance);
}
Run Tests, failed, so
 
// class Automat
public void buy(Pepsi pepsi)
{
  goods.remove(pepsi);
  balance -= Pepsi.PRICE;
}
Run Tests again, OK.
 
那如果没有投币就直接买Pepsi呢?再添加一个测试:
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithNoBalance()
{
  try
  {
    automat.buy(pepsi);
    fail("a NoBalanceException was expected when buying a pepsi with no balance.");
  }
  catch (NoBalanceException e)
  {
  }
}
 
 
// class NoBalanceException
public class NoBalanceException extends Exception
{
  public NoBalanceException(String arg0)
  {
    super(arg0);
  }
}
 
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithTwoYuan()
{
  automat.put(2);
  try
  {
    automat.buy(pepsi);
  }
  catch (NoBalanceException e)
  {
    fail("a NoBalanceException was throwed.");
  } 
  assertTrue("the automat should be empty.", automat.isEmpty()); 
  assertEquals("the balance should be 0.", 0, automat.balance);
 }

 
// class Automat
public void buy(Pepsi pepsi) throws NoBalanceException
{
  if (balance == 0)
    throw new NoBalanceException("No balance");
  else if (balance >= Pepsi.PRICE)
  {
    goods.remove(pepsi);
    balance -= Pepsi.PRICE;
  }
}
 
运行测试,绿色!
接下来投1块钱买买看:
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithOneYuan()
{
  automat.put(1);
  try
  {
    automat.buy(pepsi);
    fail("a LackOfBalanceException was expected when buying a pepsi without enough balance.");
  }
  catch (LackOfBalanceException e)
  {
  }
  catch (NoBalanceException e)
  {
    fail("a NoBalanceException was not expected.");
  }
}
接下来,为了能使编译通过,做了一下修改:
// class LackOfBalanceException
public class LackOfBalanceException extends Exception
{
  public LackOfBalanceException(String arg0)
  {
    super(arg0);
  }
}
// class Automat
public void buy(Pepsi pepsi) throws NoBalanceException, LackOfBalanceException
{
  // ...
}
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithNoBalance()
{
  // ...
  catch (LackOfBalanceException e)
  {
    fail("a LackOfBalanceException was not expected.");
  }

}
 
public void testBuyPepsiWithTwoYuan()
{
  // ...
  catch (LackOfBalanceException e)
  {
    fail("a LackOfBalanceException was not expected.");
  } 
  // ...
}

好,没有错误提示了。编译,运行测试,红色进度条。
testBuyPepsiWithOneYuan()提示“a LackOfBalanceException was expected when buying a pepsi with no enough balance."
 
我们修改一下Automat.buy():
// class Automat
public void buy(Pepsi pepsi) throws NoBalanceException, LackOfBalanceException
{
  if (balance == 0)
  {
    throw new NoBalanceException("No balance");
  }
  else if (balance >= Pepsi.PRICE)
  {
    goods.remove(pepsi);
    balance -= Pepsi.PRICE;
  }
  else
    throw new LackOfBalanceException("Lack of Balance");
}
测试通过了!小小庆祝一下~
 
那如果我们投了三块钱呢?好,试试看:
 
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithThreeYuan()
{
  automat.put(3);
  assertTrue("we can buy pepsi.", automat.canBuy(pepsi));  
  try
  {
    automat.buy(pepsi);
    assertTrue("the automat should be empty.", automat.isEmpty());
    assertEquals("the balance should be 1.", 1, automat.balance);
  }
  catch (Exception e)
  {
    fail("Exception was not expected.");
  }  
}
 
编译,运行测试,成功!Yeah!
 
To be continued...
 
 
下一集将会更加精彩,敬请期待。
posted on 2006-02-25 23:12 Baoquan Inside 阅读(1607) 评论(3)  编辑  收藏 所属分类: TDD

评论

# re: 测试驱动开发案例之自动售货机(第1集) 2006-02-27 02:56 dev  回复  更多评论   

你的这个例子存在并发问题,一个是金额,另一个是商品都有这个问题;另外,一个自动售货机可能会有几种商品,这个没有考虑到

# re: 测试驱动开发案例之自动售货机(第1集) 2006-02-27 10:32 Baoquan Inside  回复  更多评论   

@dev
谢谢你的关注!我在并发编程方面几乎没有经验,所以例子可能存在你说提到的并发问题。为了写这篇文章,我一直在Blog和Eclipse之间换来换去。不过故事才刚刚开始,所以,你提到的“多种商品”问题将在下一集中被解决。

# re: 测试驱动开发案例之自动售货机(第1集) 2007-09-16 16:48 鱼上游  回复  更多评论   

第2集呢?

只有注册用户登录后才能发表评论。


网站导航: