原文:
Behavior-Driven Development with NBehave
这里模拟了一个“银行账户”的类
一个余额属性,一个存款方法,一个撤销账户的方法,一个转账的方法。
[csharp] view plaincopy
- public sealed class Account
- {
- private int balance;
- public int Balance
- {
- get { return balance; }
- set { balance = value; }
- }
- public void Deposit(int amount)
- {
- }
- public void Withdraw(int amount)
- {
- }
- public static void Transfer(Account from, Account to, int amount)
- {
- }
- }
初始化测试类(注:引用了NBehave的dll)
[csharp] view plaincopy
- public class AccountTest : SpecBase
- {
- public Account account;
- public Account account2;
- [SetUp]
- public void Initialize_before_each_test()
- {
- account = new Account();
- account2 = new Account { Balance = 100 };
- }
- }
Stories
(关于BDD)所有的内容都从一个‘故事’(Story)开始。
很早之前,NBehave的开发者们就开始尝试写出一个叫Story的类,这个类被设计出来的意义就是为了能够描述一组特定的应用场景,以用来模拟我们想要具体测试的业务。
举个例子,假设我们把这个story描述成一个存款业务,于是:
[csharp] view plaincopy
- [Story, That, Should("Increase account balance when money is deposited")]
- public void Deposit_should_increase_account_balance()
- {
- Story story = new Story("Deposit");
- story.AsA("User")
- .IWant("The bank account balance to increase by the amount deposited")
- .SoThat("I can deposit money");
- // scenarios here
- }
(注:这个story从上往下读,按字符串的描述可以读成:
存款业务应该增加账户余额
作为一个用户,我想要让账户内的余额增加,那么我可以选择通过存款实现
PS:我的语序混乱了,但原文的例子是这样写的)
整段代码其实并没有技术实现上的实际意义,它只不过是利用了一种类似第三方单元测试的语法描述了业务的特性(原文:the attributes that decorate the test method are really using the familiar xUnit testing attributes 而对于上个story,文章使用了NBehave),对于BDD而言,其目的是为了让整个业务的逻辑性和可读性增强,当然我们也可以换成别的语法风格,比如
[csharp] view plaincopy
- using That = MbUnit.Framework.TestAttribute;
- using Describe = MbUnit.Framework.CategoryAttribute;
- using For = MbUnit.Framework.CategoryAttribute;
- using Wrote = MbUnit.Framework.DescriptionAttribute;
- using Should = MbUnit.Framework.DescriptionAttribute;
其实不论那种风格,目的都是为了让业务和逻辑变的可读,而NBehave的风格已经成为了一种趋势,因为如上面所演示的一样,他的可读性太棒了。
现在回到那个story里,发现没有,整个story是没有任何test存在的,它仅仅是使用了文字与NBehave提供的接口就完成了一个完整业务的描述。
于是story的定义出来了,他就是为了更清晰的反映出一个特定需求(注意,是一个特定的需求,而不是一个无序的故事,在实际的开发中,这需要业务人员悉心的分离出来),然后当这个story送到了开发人员的手上时,他们就能有针对性的并能更容易决定写出哪些测试。
Scenarios
我们有了一个story,现在可以开始写测试了。我们根据story,然后提供一些叫做scenario的内容-描述一个可能发生的事件(原文:we have to provide something called a scenario – a description of one possible thing that can happen)
就银行存款案例而言,可能发生这些情况:你的账户能正常存款(这最好);你的账户被注销了(这当然就不能存款了);或者你输入了一个负的数字(一般人当然不会);我们要对每一种可能出现的场景做到覆盖率100%的测试,让我们先从简单的开始:
[csharp] view plaincopy
- story.WithScenario("Money deposit")
- .Given("My bank account is empty", () => { account.Balance = 0; })
- .When("I deposit 100 units", () => account.Deposit(100))
- .Then("The account balance should be 100", () => account.Balance.ShouldEqual(100));
这个小片段大概已经告诉我们什么是BDD了,是的,一个前提,一个后置条件,最后是结果
- 首先,
WithScenario()
告诉系统这是一个什么样的scenario。这段信息将被设置到测试工具中,之后的所有(测试)内容都请遵循这个场景的限定。 - 然后, 使用
Given()
来定义一个前提-即初始化一个空的账户。这里有两个参数,一个string
用来描述你将做什么, 另一个Action
将实现你之前的定义(即给定/初始化条件)。 When()
方法描述一个行为,当一个行为动作发生时该怎样处理的,这就是我们之后想要测试的内容。- 最后,
Then()
方法将对我们的方法进行测试。
(通顺的翻译:
在“存款”这个场景中,
我们有一个余额为0的账户,
现在我往账户里存入100元,
于是我的账户余额应该变成了100)
你可能已经发现了,在上面的测试中,包含了大量了lambda表达式,这是NBehave的特点之一,可以灵活的配置每一个Action。
现在来看看测试结果(注:文章中使用的是MbUnit,写出这个测试的主要目的是让大家可以很清晰的看到NBehave所带来的好处——结构清晰,通俗易懂)
[csharp] view plaincopy
- *** DebugTrace ***
- Story: Deposit
- Narrative:
- As a User
- I want The bank account balance to increase by the amount deposited
- So that I can deposit money
- Scenario 1: Money deposit
- Given My bank account is empty
- When I deposit 100 units
- Then The account balance should be 100 - FAILED
- MbUnit.Core.Exceptions.NotEqualAssertionException:
- Equal assertion failed: [[0]]!=[[100]]
很显然,因为我们根本没有实现Deposit方法,测试当然不能通过,现在我们想写办法(添加一些内容)来让测试通过。
[csharp] view plaincopy
- public void Deposit(int amount)
- {
- balance += amount;
- }
测试通过了,瞧,多简单!通过业务分析,描述场景以及测试,我们实现了一个业务方法了。
再看一个复杂点的,在存款时尝试存一个负的数
[csharp] view plaincopy
- story.WithScenario("Negative amount deposit")
- .Given("My bank account is empty", () => { account.Balance = 0; })
- .When("I try to deposit a negative amount", () => { })
- .Then("I get an exception",
- () => typeof(Exception).ShouldBeThrownBy(() => account.Deposit(-100)))
- .And("My bank account balance is unchanged",
- () => account.Balance.ShouldEqual(0));
这里注意两件事,一是在Then()中使用扩展方法ShouldBeThrownBy(),确保调用了 Deposit()方法添加一个负数,另一个是使用And来验证余额(注:And相当于重载了一次Then()方法,它在使用在Given(),When()的后面时,是同样的意义,表示重载上一个方法)
(关于为什么使用 When("I try to deposit a negative amount", () => { })这样一个空方法,作者的解释大概是说想要通过这样一个不合理的方法来捕捉到一个异常,具体不翻译了,和NBehave关系不大,是他例子中的一种想法)
跑一遍测试,然后看输出:
[csharp] view plaincopy
- *** DebugTrace ***
- Story: Deposit
- Narrative:
- As a User
- I want The bank account balance to increase by the amount deposited
- So that I can deposit money
- Scenario 1: Money deposit
- Given My bank account is empty
- When I deposit 100 units
- Then The account balance should be 100
- Scenario 2: Negative amount deposit
- Given My bank account is empty
- When I try to deposit a negative amount
- Then I get an exception - FAILED
我们得到了一个异常,第二个场景没有通过,于是我们继续想办法来让测试通过:
[csharp] view plaincopy
- public void Deposit(int amount)
- {
- if (amount <= 0)
- throw new Exception();
- balance += amount;
- }
同样是一个很简单的方法,继续通过分析,举例场景以及测试,又完成了一个业务需求。
上面那个场景的语法很明显是不易懂的,不但使用了一个扩展方法,并且还没有一个行为发生(空的When()方法),很不合逻辑
更改一下场景:
[csharp] view plaincopy
- story.WithScenario("Valid transfer")
- .Given("I have 100 dollars", () => { account.Balance = 100; })
- .And("You have 100 dollars", () => { account2.Balance = 100; })
- .When("I give you 50 dollars",
- () => Assert.DoesNotThrow(() => Account.Transfer(account, account2, 50)))
- .Then("I have 50 dollars left", () => account.Balance.ShouldEqual(50))
- .And("You have 150 dollars", () => account2.Balance.ShouldEqual(150));
整个例子完成了,没有扩展方法,没有奇怪的语句,整个测试和实现就是由语句和NBehave提供的接口来完成的。