敏捷软件开发

发表于:2008-2-29 17:23

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:网络转载

  下一个要进行的测试是从牌桌上发牌。当我们在为测试方法编写代码的时候,我们所扮演的角色就是将要编写的应用程序的用户。这就是为什么我们给自己的类创建的接口要与给用户的接口像类似的原因。在本文的这个例子里,我们将按照命令/查询分离原则(Command/Query Separation Principle5)编写出下面这样的代码。

Deck类。如列表A所示。

列表A

import java.util.List;import java.util.ArrayList;public class Deck{private static final int CARDS_IN_DECK = 52;private List cards = new ArrayList();public boolean isEmpty(){return size() == 0;}public int size(){return cards.size();}public void add(int card) throws IllegalStateException{if(CARDS_IN_DECK == size())throw new IllegalStateException("Cannot add more than 52 cards");cards.add(new Integer(card));}public int top(){return ((Integer) cards.get(0)).intValue();}public void remove(){cards.remove(0);}}

  我们所有的测试都通过了,而且我们没有看到任何重复或者其他必要的重整,所以应该是时候进行下面的测试了。然而事实却不是这样的。我们top和remove方法的实现里有一个潜在的问题。如果对一个空的Deck调用它们,会发生什么?这两个方法都会从纸牌的内部列表里跳出一个IndexOutOfBoundsException异常,但是目前我们还没有就这个问题进行沟通。回头看看简单性的原则,我们知道自己需要沟通。我们的类的用户应该知道这个潜在的问题。幸运的是,我们将这种测试当作是一种沟通的方式,因此我们增加了下面的测试。

public void testTopOnEmptyDeck(){Deck deck = new Deck();try{deck.top();fail("IllegalStateException not thrown");}catch(IllegalStateException e){assertEquals("Cannot call top on an empty deck", e.getMessage());}}public void testRemoveOnEmptyDeck(){Deck deck = new Deck();try{deck.remove();fail("IllegalStateException not thrown");}catch(IllegalStateException e){assertEquals("Cannot call remove on an empty deck", e.getMessage());}}

  上面都是异常测试(Exception Test2)的例子。我们再一次运行这些测试看它们失败,然后加入实现让它们通过。

public int top(){if(isEmpty())throw new IllegalStateException("Cannot call top on an empty deck");return ((Integer) cards.get(0)).intValue();}public void remove(){if(isEmpty())throw new IllegalStateException("Cannot call remove on an empty deck");cards.remove(0);}

  尽管guard语句有重复,但是我们决定不去管它,没有将它们简化成一个共同的方法。这是因为沟通的价值超过了重复的代价,当然这只是一个个人的选择。

一手牌
  我们已经完成了对牌桌和投注台面的测试和实现,现在就到了创建一手牌的时候了。待办事项列表再一次发挥其作用,我们得到了下面这样一个列表:

·         创建一个一开始没有纸牌的空手

·         向手上加入纸牌

·         检查一只手是否击败了另一手

·         检查一只手是否爆了

  为空手增加一个测试很简单,我们继续到给手上加入纸牌。

public void testAddACard()
{
  Hand hand = new Hand();
  hand.add(10);
  assertEquals(1, hand.size());
  hand.add(5);
  assertEquals(2, hand.size());
}

  我们运行测试,然后加入实现。

public void add( int card )
{
  cards.add(new Integer(card));
}

  测试通过了,我们没有看到Hand类里有任何重复。但是我们刚刚给Hand加上的实现和给Deck加上的方法极其相似。回头看看牌桌的待办事项列表,我们记得必须检查牌桌(上纸牌的张数)是否正确,我们最后也对手做同样的事情。

public void testAddInvalidCard(){Hand hand = new Hand();try{hand.add(1);fail("IllegalArgumentException not thrown");}catch(IllegalArgumentException e){assertEquals("Not a valid card value 1", e.getMessage());}try{hand.add(12);fail("IllegalArgumentException not thrown");}catch(IllegalArgumentException e){assertEquals("Not a valid card value 12", e.getMessage());}}

  我们加入了下面的实现来通过测试。

public void add( int card )
{
  if(card < 2 || card > 11)
    throw new IllegalArgumentException("Not a valid card value " + card);
  cards.add(new Integer(card));
}

  但是现在我们在Deck和Hand里有相同的guard语句,用来检查该自变量是否代表着正确的纸牌值。简单性的原则要求我们删除重复,但是在这里情况并不像Extract Method重整6这么简单。如果我们看到多个类之间存在重复,这意味着我们缺失了某种概念。在这里我们很容易就看到Card类担负起了判断什么值是有效的责任,而Deck和Hand作为Card的容器变得更具沟通性。

  我们引入了Card类以及相应的Deck和Hand重整,如列表B:

public class Card{private final int value;public Card( int value ){if( value < 2 || value > 11 )throw new IllegalArgumentException( "Not a valid card value " + value );this.value = value;}public int getValue(){return value;}}public class Deck{private static final int[] NUMBER_IN_DECK = new int[] {0, 0, 4, 4, 4, 4, 4, 4, 4,4, 16, 4};…public void add( Card card ) throws IllegalStateException{if(NUMBER_IN_DECK[card.getValue()] == countOf(card))throw new IllegalStateException("Cannot add more cards of value " +card.getValue());cards.add(card);}public Card top(){if(isEmpty())throw new IllegalStateException("Cannot call top on an empty deck");return (Card) cards.get(0);}…private int countOf(Card card){int result = 0;for(Iterator i = cards.iterator(); i.hasNext(); ){Card each = (Card) i.next();if(each.getValue() == card.getValue())result++;}return result;}}public class Hand{…public void add( Card card ){cards.add(card);}…}

  测试-编码-重整循环的每一阶段都涉及不同类型的思想。在测试阶段,重点放在了被实现的类的接口上。编写代码是为了让测试尽可能快地通过测试。而重整阶段可以被当作是使用简单性原则进行指导的微型代码审查。有没有重复的或者看起来类似的代码,不仅仅是在当前的类里,而且是在系统的其他类里?现在的实现可能会出现什么问题,类的用户能够与之顺利沟通吗?

重要的成功因素

·         小步前进——TCR对于开发人员来说不是一个很容易的转换。一次只进行一个步骤,同时还要明白它学习起来有一定难度。

·         严格遵守原则——只进行TDD或者只进行重整并不能让整个TCR循环一蹴而就。给自己足够的时间来尝试,并取得效果。压力和最终期限会迫使小组回到原来的习惯上——一定要小心!

·         重整过程——与小组的所有成员交换意见,了解一下他们的反馈

·         理解——确保整个小组都完全理解TCR循环是什么,如何实现它。考虑一下就此主题进行员工培训和讲座。

配对编程——第二步
  TCR循环可以由某个开发人员独自完成,但是敏捷开发和TCR循环的真正威力来自于配对编程(pair programming)。在敏捷开发里,开发人员每两人一组编写所有的生产代码,其中一人担当“驱动者(driver)”(负责操作鼠标和键盘),而另一个人同驱动者一道解决问题和规划更大的图景。编程配对里的这个驱动者可以按需要进行轮换。配对让你能够实现眼前的目标,同时确保不会忽略项目的整体目标。它会保证有人在考虑下一步的走向和下一个要解决的问题。

  虽然配对编程引起了很多争议,但是大多数优秀的开发人员还是在按照这一方法进行开发,至少有的时候是这样的。管理人员们可能会相信配对编程降低了生产效率,然而尽管开发小组的生产效率在一开始会有所降低,但是研究已经表明从质量和增加的生产效率的角度来看,配对编程远远超过了开发人员单独工作的质量和效率7。而另一方面,开发人员可能会觉得配对编程非常困难,因为它需要与人们更多的交互过程,并与另一个开发人员一起编写代码。但是这也是建立一种相互学习的环境的最好方法。

实施配对编程

1.       不要独断专行——要讨论。与你的小组成员讨论配对编程的思想及其优劣,而不是独断专行地给他们定规则。配对编程是开发人员相互学习的绝好机会。

2.       确定你的小组需要多少配对。配对编程是一项工作强度很大但是令人满意的工作方式。

3.       不要让配对编程人员每天连续工作八个小时——否则你的小组会吃不消的。从较短的时间开始——每天一到两个小时,看看它是如何进展的,然后随着小组信心的增强而延长时间。

4.       定期检查。如果你已经决定尝试再次进行敏捷开发,你就需要确保为小组营造了正式的环境,以便(定期)就项目进度进行反馈。

《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号