測(cè)試驅(qū)動(dòng)開發(fā)案例之自動(dòng)售貨機(jī)(第1集)
測(cè)試驅(qū)動(dòng)開發(fā)(TDD, Test Driven Development)是一種很有意思的軟件開發(fā)方式,本集以較小的步伐體驗(yàn)TDD。
/** A program simulation of an automat.
/** A program simulation of an automat.
*
* Director: Zuo Baoquan
* Contact me:
* Email: Baoquan.Zuo [at] gmail.com
*
* Copyright 2006 BEC Studio
*/
引言:
(該問題截取自《面向?qū)ο蟮脑O(shè)計(jì)與模式》(Written by Cay Horstmann) Exercise 2.18
設(shè)計(jì)并實(shí)現(xiàn)一個(gè)模擬自動(dòng)售貨機(jī)的程序。通過給機(jī)器投入正確的硬幣,便可以購(gòu)買到相應(yīng)的商品。用戶從一個(gè)可用的商品列表中選擇商品、投入硬幣并得到商品。如果沒有足夠的錢幣或由于此商品已銷售完畢,則將硬幣退回。操作員可以重新備貨并將錢幣取走。
場(chǎng)景#1
地點(diǎn):同濟(jì)大學(xué)西南樓
涉眾:我,空自動(dòng)售貨機(jī)
寢室樓里面剛搬來了一臺(tái)空的自動(dòng)售貨機(jī),那我們來測(cè)試一下:
public class TestEmptyAutomat extends TestCase
{
public void testIsEmpty()
{
Automat automat = new Automat();
{
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
{
}
}
{
public Automat() // constructor stub
{
}
}
再露一手:
//class Automat
public boolean isEmpty()
{
return true;
}
全部保存,運(yùn)行測(cè)試,呵呵,綠色進(jìn)度條!測(cè)試成功!
好,既然我們還沒有投過硬幣,那么余額應(yīng)該是0了~
// class TestEmptyAutomat
public void testBalance(){
Automat automat = new Automat();
assertEquals("the balance should be 0.", 0, automat.balance);
}
繼續(xù)使用我們的法寶:
// class Automat
public final int balance = 0;
運(yùn)行測(cè)試,yeah!
看了一遍測(cè)試程序,決定優(yōu)化一下:
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;
}
{
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;
}
再運(yùn)行一次測(cè)試,呵呵,又是綠色!
場(chǎng)景#2
地點(diǎn):同濟(jì)大學(xué)西南樓
涉眾:我,自動(dòng)售貨機(jī)(有一瓶百事可樂)
好消息,樓長(zhǎng)在自動(dòng)售貨機(jī)里面放了一瓶Pepsi。
public class TestAutomatWithOnePepsi extends TestCase
{
protected void setUp() throws Exception
{
super.setUp();
automat = new Automat();
{
protected void setUp() throws Exception
{
super.setUp();
automat = new Automat();
pepsi = new Pepsi(); // 一瓶百事可樂
automat.add(pepsi); // 樓長(zhǎng)阿姨放的~
}
}
public void testEmpty()
{
assertFalse("it should not be empty.", automat.isEmpty());
}
public Automat automat;
{
assertFalse("it should not be empty.", automat.isEmpty());
}
public Automat automat;
public Pepsi pepsi;
}
}
接著創(chuàng)建Pepsi類
public class Pepsi
{
public Pepsi() // constructor stub
{
}
}
{
public Pepsi() // constructor stub
{
}
}
再給Automat添加add方法:
// class Automat
public void add(Pepsi pepsi)
{
}
好,現(xiàn)在有兩個(gè)TC(Test Case)了,為了運(yùn)行兩個(gè)測(cè)試案例,我們來創(chuàng)建一個(gè)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$
{
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;
}
}
}
}
編譯成功,運(yùn)行AutomatTests,紅色進(jìn)度條。TestEmptyAutomat綠色,TestAutomatWithOnePepsi紅色。呵呵,看來要讓add做點(diǎn)事情了。
// class Automat
public void add(Pepsi pepsi)
{
goods.add(pepsi);
}
// 添加一個(gè)裝Pepsi的數(shù)組列表
private final ArrayList<Pepsi> goods = new ArrayList<Pepsi>();
// 修改isEmpty方法
public boolean isEmpty()
{
return goods.isEmpty();
}
{
return goods.isEmpty();
}
再次運(yùn)行AutomatTests,呵呵,綠色!我們喜歡!
好,我們?cè)倏纯碅utomat的余額:
// class TestAutomatWithOnePepsi
public void testBalance()
{
assertEquals("the balance should be 0.", 0, automat.balance);
}
{
assertEquals("the balance should be 0.", 0, automat.balance);
}
運(yùn)行一遍測(cè)試,Ok。
我們還沒有投硬幣,當(dāng)然不能買百事可樂了:
// class TestAutomatWithOnePepsi
public void testCanBuyWithoutBalance()
{
assertFalse("we cannot buy pepsi without money.", automat.canBuy(pepsi));
}
{
assertFalse("we cannot buy pepsi without money.", automat.canBuy(pepsi));
}
// class Automat
public boolean canBuy(Pepsi pepsi)
{
return false;
}
我們太喜歡運(yùn)行測(cè)試了,于是又忍不住運(yùn)行了所有的自動(dòng)測(cè)試(呵呵,實(shí)際上我們只需要點(diǎn)擊一個(gè)運(yùn)行按鈕)。又是綠色~
好,如果Pepsi的價(jià)格是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));
}
assertFalse("we cannot buy pepsi with one yuan.", automat.canBuy(pepsi));
}
// class Automat
public void put(int yuan)
{
}
運(yùn)行測(cè)試,綠色。顯然1塊錢買不到百事可樂。那就投2塊錢吧:
// class TestAutomatWithOnePepsi
public void testCanBuyWithTwoYuan()
{
automat.put(2);
assertTrue("we can not buy pepsi with two yuan.", automat.canBuy(pepsi));
}
{
automat.put(2);
assertTrue("we can not buy pepsi with two yuan.", automat.canBuy(pepsi));
}
運(yùn)行測(cè)試,紅色進(jìn)度條,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
迫不及待地點(diǎn)擊了運(yùn)行按鈕,yeah!終于能買到喜歡的Pepsi了(因?yàn)榭吹搅司G色進(jìn)度條~)。
于是急忙買了一瓶Pepsi:
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithTwoYuan()
{
automat.put(2);
automat.buy(pepsi);
{
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);
{
automat.put(2);
automat.buy(pepsi);
assertTrue("the automat should be empty.", automat.isEmpty());
assertEquals("the balance should be 0.", 0, automat.balance);
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呢?再添加一個(gè)測(cè)試:
// class TestAutomatWithOnePepsi
public void testBuyPepsiWithNoBalance()
{
try
{
automat.buy(pepsi);
fail("a NoBalanceException was expected when buying a pepsi with no balance.");
}
catch (NoBalanceException e)
{
}
}
{
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);
}
}
{
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);
}
{
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)
{
if (balance == 0)
throw new NoBalanceException("No balance");
else if (balance >= Pepsi.PRICE)
{
goods.remove(pepsi);
balance -= Pepsi.PRICE;
}
}
else if (balance >= Pepsi.PRICE)
{
goods.remove(pepsi);
balance -= Pepsi.PRICE;
}
}
運(yùn)行測(cè)試,綠色!
接下來投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.");
}
}
{
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);
}
}
{
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.");
}
}
catch (LackOfBalanceException e)
{
fail("a LackOfBalanceException was not expected.");
}
}
public void testBuyPepsiWithTwoYuan()
{
// ...
{
// ...
catch (LackOfBalanceException e)
{
fail("a LackOfBalanceException was not expected.");
}
// ...
}
好,沒有錯(cuò)誤提示了。編譯,運(yùn)行測(cè)試,紅色進(jìn)度條。
{
fail("a LackOfBalanceException was not expected.");
}
// ...
}
好,沒有錯(cuò)誤提示了。編譯,運(yùn)行測(cè)試,紅色進(jìn)度條。
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");
}
{
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");
}
測(cè)試通過了!小小慶祝一下~
那如果我們投了三塊錢呢?好,試試看:
// 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);
{
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.");
}
}
{
fail("Exception was not expected.");
}
}
編譯,運(yùn)行測(cè)試,成功!Yeah!
To be continued...
下一集將會(huì)更加精彩,敬請(qǐng)期待。
# re: 測(cè)試驅(qū)動(dòng)開發(fā)案例之自動(dòng)售貨機(jī)(第1集) 2006-02-27 02:56 dev 回復(fù) 更多評(píng)論
你的這個(gè)例子存在并發(fā)問題,一個(gè)是金額,另一個(gè)是商品都有這個(gè)問題;另外,一個(gè)自動(dòng)售貨機(jī)可能會(huì)有幾種商品,這個(gè)沒有考慮到