使用测试驱动开发设计应用程序
在本文中,我们将探讨如何使用测试驱动开发来设计应用程序。
引言
应用程序设计是创建应用程序最重要的方面之一。设计就像应用程序的支柱。不正确的设计会导致延误,甚至更糟,会摧毁应用程序。在本文中,我们将探讨如何使用测试驱动开发来设计应用程序。
通过测试驱动开发来演进设计
人们通常认为测试驱动开发的过程是针对不同输入测试应用程序。但测试驱动开发远不止输入和结果。测试驱动开发允许开发人员设计应用程序,并确保开发人员只编写生成所需输出的代码。
本文的目的是向您展示应用程序设计如何通过测试驱动开发得以演进。
场景
没有实际场景可以讨论。我们将从测试两个类开始:Person
和 Address
。一个 Person
可以有多个 Addresses
。一个 Address
必须属于一个 Person
。随着我们继续,我们将发现用户故事和功能会不断增长,单元测试将充当保护我们免受这些变更影响的盾牌。
Person 实体
根据客户的要求,Person
必须具有以下属性:
FirstName
:人的名字LastName
:人的姓氏MiddleName
:人的中间名DOB
:出生日期DateCreated
:创建人条目的日期和时间DateModified
:修改人条目的日期和时间
Person
实体类如下所示:
public class Person
{
private string _firstName;
private string _lastName;
private DateTime _dob;
private int _id;
public int Id
{
get { return _id; }
internal set { _id = value; }
}
public string FirstName
{
get { return _firstName; }
set {
_firstName = value;
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
}
}
public DateTime DOB
{
get { return _dob; }
set {
_dob = value;
}
}
}
测试什么?
开发人员经常遇到的一个常见问题是如何开始进行测试驱动开发。我们应该如何找出要测试的模块?最好的方法是研究应用程序中哪些模块被广泛使用。找到模块后,就可以开始编写单元测试了。良好的代码覆盖率工具还将揭示有关应用程序模块使用情况的重要信息。
考虑到这一点,我们拿起电话联系了客户,讨论了应用程序的约束。客户向我们解释了以下几点:
- 名字不能为空
- 姓氏不能为空
- 人必须年满 18 岁
这为我们测试应用程序提供了一个良好的起点。让我们开始确保满足上述期望。
实现单元测试
这是我们的第一个测试用例,它确保在无效状态下不设置属性。
[RowTest]
[Row("","","02/09/2000")]
public void should_not_set_person_properties_if_in_invalid_state(
string firstname, string lastname,DateTime dob)
{
Person person = new Person() { FirstName= firstname, LastName = lastname,
DOB = dob };
Assert.IsTrue(person.FirstName != firstname,"FirstName has been set!");
Assert.IsTrue(person.LastName != lastname,"LastName has been set!");
Assert.IsTrue(person.DOB != dob,"DOB has been set!");
}
名为“should_not_set_person_properties_if_in_invalid_state
”的测试确保仅在有效状态下才设置 person 属性。我们使用的是 MbUnit 框架提供的 RowTest
属性。RowTest
属性使我们能够使用 Row
属性提供多个测试输入。您可以通过访问 Mb.unit 网站了解有关这些属性和 MbUnit 框架的更多信息。
当您运行上述测试时,它将失败。这是因为我们尚未对 Person
实体类进行任何更改,值将被设置。因此,我们需要在 Person
类属性中添加约束。让我们看看这些约束。
public class Person
{
private string _firstName;
private string _lastName;
private DateTime _dob;
public const int LEGAL_AGE = 18;
public string FirstName
{
get { return _firstName; }
set {
if(!String.IsNullOrEmpty(value.Trim()))
{
_firstName = value;
}
}
}
public string LastName
{
get { return _lastName; }
set
{
if (!String.IsNullOrEmpty(value.Trim()))
{
_lastName = value;
}
}
}
public DateTime DOB
{
get { return _dob; }
set {
// the person must be 18 or older!
if ((DateTime.Now.Subtract(value).Days / 365) >= LEGAL_AGE)
{
_dob = value;
}
}
}
}
在上面的代码中,我们添加了几个不同的约束,它们将帮助我们将对象保持在有效状态。现在,如果您运行测试,它将通过。
现在,让我们测试 Person
对象是否已成功保存。看看下面的测试:
[Test]
[RollBack]
public void should_check_if_the_person_saved_successfully()
{
Person person = new Person() { FirstName = "Mohammad", LastName = "Azam",
DOB= DateTime.Parse("02/09/1981") };
person.Save();
Assert.IsTrue(person.Id > 0);
Person vPerson = Person.GetById(person.Id);
Assert.AreEqual(person.FirstName, vPerson.FirstName);
Assert.AreEqual(person.LastName, vPerson.LastName);
Assert.AreNotEqual(person.DOB, vPerson.DOB);
}
如果运行上述测试,它将失败,因为 Person
对象没有公开 Save
或 GetById
方法。让我们添加这两个方法:
public void Save()
{
// this means adding a new person
if (this.Id <= 0)
{
SQLDataAccess.SavePerson(this);
}
else
{
SQLDataAccess.UpdatePerson(this);
}
}
public static Person GetById(int id)
{
return SQLDataAccess.GetPersonById(id);
}
Save
方法使用 SQLDataAccess
数据访问类将 Person
插入数据库。GetById
方法将“id
”作为参数,并返回与“id
”匹配的 Person
对象。
如果您处于开发的早期阶段,并且不想处理数据访问层,那么您始终可以模拟它。模拟意味着您将伪造数据访问层,因为它尚不存在。一旦实现数据访问层,您就可以直接插入它,而无需更改任何测试代码。
让我们继续处理 Address
实体。
public class Adress
{
private int _id;
private string _street;
private Person _person;
public int Id
{
get { return _id; }
internal set { _id = value; }
}
public string Street
{
get { return _street; }
set { _street = value; }
}
public Person Person
{
get { return _person; }
set { _person = value; }
}
public Address(Person person)
{
this.Person = person;
}
Address
类包含一个 Person
对象,因为 Address
属于某个特定的 Person
。需要注意的一点是 Address
对象构造函数,它接受一个 Person
对象。
Public Address(Person person)
{
_this.Person = person;
}
这是为了确保 Address
必须有一个 Person
对象。
这个过程也称为依赖注入。依赖注入有两种类型:构造函数注入和属性注入。在上面的示例中,我们使用了构造函数依赖注入。
这里有一个测试,用于确保 Address
必须有一个 Person
对象,或者 Address
必须属于一个 Person
。
[Test]
public void should_make_sure_that_an_address_must_have_a_person()
{
Address address = new Address(new Person());
Assert.IsNotNull(address.Person);
}
很简单,对吧!
现在,让我们尝试将一个 Address
添加到一个 Person
对象。这是测试:
[Test]
public void should_be_able_to_add_an_address_to_the_person()
{
Person person = new Person() { FirstName = "Mohammad", LastName = "Azam" };
Address address = new Address(person);
person.AddAddress(address);
Assert.AreEqual(1, person.Addresses.Count());
}
如果您运行上述测试,它将失败,因为 Person
对象没有 AddAddress
方法和 Addresses
属性。所以,让我们添加这两项。
private IList<Address> _addresses = new List<Address>();
public IList<Address> Addresses
{
get { return new ReadOnlyCollection<Address>(_addresses); }
}
public void AddAddress(Address address)
{
address.Person = this;
_addresses.Add(address);
}
现在如果您运行测试,它将通过。
结论
在本文中,我们学习了如何开始进行测试驱动开发,以及 TDD 方法如何帮助我们设计出更好的应用程序。在下一篇文章中,我们将看到当遇到失败的测试时,我们的设计如何变化。
希望您喜欢这篇文章 — 祝您编码愉快!