简介
在这篇文章中,我将描述一个可伸缩的{A},数据库驱动的。NET应用程序。该套件将定义用于轻松创建虚拟测试数据样本发电机,它会使用提高可扩展性测试夹具的继承和共同的功能,让简单的测试。
我将重点放在{A2}一个示例应用程序的测试。这通常是位于应用程序的"中间",通常被称为业务逻辑层(BLL)。它使用的数据访问层(DAL)进行调解,并从数据库中的数据传输和驱动器的一个或多个用户界面(UI)与用户交互或显示在其中的数据的行为。
本文假设。NET和C#的知识,但并不需要单元测试或NUnit的特别经验。BLL的实施
在此示例应用程序,在BLL中实现数据库操作的类称为持久性对象的基类继承。这个类定义如下界面[{A3}]:public abstract class PersistentObject {
protected long _uid = long.MinValue;
/// <summary>
/// The unique identifier of this object
/// in the database
/// </summary>
/// <remarks>
/// Set when Fill() is called
/// </remarks>
public long UID {
get { return _uid; }
}
/// <summary>
/// Save this object's data to the database.
/// </summary>
public abstract void Save();
/// <summary>
/// Fill this object with data fetched from the
/// database for the given UID
/// </summary>
/// <param name="uid">The unique identifier of
/// the record to fetch from the database</param>
public abstract void Fill(long uid);
/// <summary>
/// Remove this object from the database
/// </summary>
public abstract void Delete();
/// <summary>
/// Fetches an object of the given type and with the
span>
/// given UID from the database
/// </summary>
/// <typeparam name="ConcreteType">
/// The type of object to fetch
/// </typeparam>
/// <param name="uid">
/// The unique identifier of the object in the database
/// </param>
public static ConcreteType Fetch<ConcreteType>(long uid)
where ConcreteType : PersistentObject, new() {
ConcreteType toReturn = new ConcreteType();
toReturn.Fill(uid);
return toReturn;
}
}
说,例如,应用程序必须保存一些客户数据和客户端的地址,可用于在应用程序。 BLL的,因此需要包含从持久性对象的地址和客户端类。{C}
客户端是相似的,但它包含了一个属性,它返回的客户端的地址对象。public class Client : PersistentObject {
private string _firstName = null;
private string _lastName = null;
private string _middleName = null;
private long _addressUID = long.MinValue;
private Address _addressObject;
// ...
public long AddressUID {
get { return _addressUID; }
set { _addressUID = value; }
}
/// <summary>
/// On-demand property that returns this Client's
/// Address based on the current value of AddressUID<
/span>
/// </summary>
public Address Address {
get {
if (AddressUID == long.MinValue) {
_addressObject = null;
}
else if (_addressObject == null
|| AddressUID != _addressObject.UID) {
_addressObject = new Address();
_addressObject.Fill(AddressUID);
}
return _addressObject;
}
}
// ...
}
要保存新的客户端数据,用户将做类似以下内容:
// Create the address that the client will link to
Address newAddress = new Address();
newAddress.StreetAddress = StreetAddressInput.Text;
newAddress.City = CityInput.Text;
newAddress.State = StateInput.Text;
newAddress.Zip = ZipInput.Text;
// Save the address to the database
newAddress.Save();
// Create the client
Client newClient = new Client();
newClient.FirstName = FirstNameInput.Text;
newClient.MiddleName = MiddleNameInput.Text;
newClient.LastName = LastNameInput.Text;
// Link to the address
newClient.AddressUID = newAddress.UID;
// Save the client to the database
newClient.Save();
和检索客户端数据应用程序,用户会做类似以下内容:
单元测试背景Client existingClient = Client.Fetch(clientUID);
Address clientAddress = existingClient.Address;
上文所述的BLL实施是比较标准的。一个可以验证其行为在任何数量的方式。最简单的,但至少强大的测试UI。由于UI BLL的依赖,可以想见,验证通过网页或对话框手工运行的应用程序。但是,如果应用程序有多个用户界面?显然,这种方法很慢,难以重复,容易出现人为错误,并可能会错过错误。此外,它可促进在一个殿的编码器可固定在一种症状,而不是在BLL基础事业的UI不好的编程习惯。这是不是说,我们应该省略的UI测试,只是我们不应该依靠它来验证业务逻辑。
一个更好的选择将创建一个简单的驱动程序正在开发的程序调用BLL方法。此选项将肯定会更容易重复,但可能很难保存司机稍后或运行所有现有的驱动程序,以验证没有被打破。
这是{A4}进来一个人,可以看作是一个单元测试一个简单的驱动程序,可能会写反正。单元测试框架组织的测试,提供工具,使编写测试变得更容易,并允许一个总运行测试。测试套件实现
由于本文的讨论。NET应用程序,我将使用{A5},使得很容易编写和运行测试。
这是最直观的BLL中的每个类创建一个测试夹具(即,一个类包含了一系列的测试)。因此,在我们的例子,例如测试套件ClientTest和AddressTest类。这些基本的测试夹具将需要验证数据添加到数据库中,检索,编辑,并正确地删除。我们经常需要创建虚拟对象,因此,这些测试装置还包括一些示例发电机。最后,我们不希望重复在许多不同的测试夹具的共同的测试代码,因此我们将测试在PersistentObjectTest类ClientTest和AddressTest都继承了常见的数据库操作。
我会解释的PersistentObjectTest的部分建设。首先,在类声明:/// <summary>
/// Abstract base class for test fixtures that test
/// classes derived from BLL.PersistentObject
/// </summary><
/span>
/// <typeparam name="PersistentObjectType">
/// The type of BLL.PersistentObject that the derived
/// class tests
/// </typeparam>
public abstract class PersistentObjectTest<PersistentObjectType>
where PersistentObjectType : PersistentObject, new() {
这表明PersistentObjectTest接受对象类型的泛型类型,其派生类测试。这种类型来自持久性对象和一个空的构造。这让我们创建一个类型安全的,通用的方式样品发电机和其他公用事业:#region Sample Generators
/// <summary>
/// Returns a dummy object
/// </summary><
/span>
/// <param name="type">
/// Indicates whether the returned dummy object should
/// be saved to the database or not
/// </param>
public PersistentObjectType GetSample(SampleSaveStatus saveStatus) {
PersistentObjectType toReturn = new PersistentObjectType();
FillSample(toReturn);
if (saveStatus == SampleSaveStatus.SAVED_SAMPLE) {
toReturn.Save();
// Check Save() postconditions...
}
return toReturn;
}
/// <summary>
/// Fills the given object with random data
/// </summary><
/span>
/// <param name="sample">
/// The sample object whose fields to fill
/// </param>
/// <remarks>
/// Should be overridden and extended in
/// derived classes
/// </remarks><
/span>
public virtual void FillSample(PersistentObjectType sample) {
// nothing to fill in the base class
}
/// <summary>
/// Asserts that all fields in the given objects match
/// </summary><
/span>
/// <param name="expected">
/// The object whose data to check against
/// </param>
/// <param name="actual">
/// The object whose fields to test
/// </param>
/// <remarks>
/// Should be overridden and extended in
/// derived classes
/// </remarks><
/span>
public virtual void AssertIdentical
(PersistentObjectType expected, PersistentObjectType actual) {
Assert.AreEqual(expected.UID, actual.UID,
"UID does not match");
}
#endregion
GetSample()只是简单地返回一个虚拟的对象。 FillSample()和AssertIdentical()的实现委托给派生类。这三种方法用于其他测试夹具,创建和测试样品对象。基类使用它们来验证在下面的测试方法的基本的数据库操作:#region Data Tests
/// <summary>
/// Tests that data is sent to and retrieved from
/// the database correctly
/// </summary><
/span>
[Test()]
public virtual void SaveAndFetch() {
PersistentObjectType original =
GetSample(SampleSaveStatus.SAVED_SAMPLE);
PersistentObjectType fetched =
PersistentObject.Fetch<PersistentObjectType>(original.UID);
// verify that the objects are identical
AssertIdentical(original, fetched);
}
/// <summary>
/// Tests that editing an existing object works correctly<
/span>
/// </summary><
/span>
[Test()]
public virtual void EditAndFetch() {
PersistentObjectType modified =
GetSample(SampleSaveStatus.SAVED_SAMPLE);
// edit fields
FillSample(modified);
// save edits
modified.Save();
// make sure edits were reflected in the database
PersistentObjectType fetched =
PersistentObject.Fetch<PersistentObjectType>(modified.UID);
AssertIdentical(modified, fetched);
}
/// <summary>
/// Tests that deletion works correctly.
/// </summary><
/span>
/// <remarks>
/// Expects data retrieval to fail
/// </remarks><
/span>
[Test(),
ExpectedException(typeof(DataNotFoundException))]
public virtual void Delete() {
PersistentObjectType toDelete =
GetSample(SampleSaveStatus.SAVED_SAMPLE);
long originalUID = toDelete.UID;
toDelete.Delete();
// expect failure because the object does not exist
PersistentObject.Fetch<PersistentObjectType>(originalUID);
}
#endregion
PersistentObjectTest做繁重的,具体的测试类只需要定义如何填写样本对象,以及如何检查,如果两个样本对象是相同的。他们还可以定义额外的样品发电机,实用功能和测试方法,根据需要。[TestFixture()]
public class AddressTest : PersistentObjectTest<Address> {
public override void FillSample(Address sample) {
base.FillSample(sample);
Random r = new Random();
string[] states = {"IL", "IN", "KY", "MI"};
sample.City = "CITY" + DateTime.Now.Ticks.ToString();
sample.State = states[r.Next(0, states.Length)];
sample.StreetAddress = r.Next().ToString() + " Anywhere Street";
sample.Zip = r.Next(0, 100000).ToString("00000");
}
public override void AssertIdentical(Address expected, Address actual) {
base.AssertIdentical(expected, actual);
Assert.AreEqual(expected.City, actual.City,
"City does not match");
Assert.AreEqual(expected.State, actual.State,
"State does not match");
Assert.AreEqual(expected.StreetAddress, actual.StreetAddress,
"StreetAddress does not match");
Assert.AreEqual(expected.Zip, actual.Zip,
"Zip does not match");
}
}
[TestFixture()]
public class ClientTest : PersistentObjectTest<Client> {
public override void FillSample(Client sample) {
base.FillSample(sample);
sample.FirstName = "FIRST" + DateTime.Now.Ticks.ToString();
sample.MiddleName = "MIDDLE" + DateTime.Now.Ticks.ToString();
sample.LastName = "LAST" + DateTime.Now.Ticks.ToString();
sample.AddressUID = new AddressTest().GetSample
(SampleSaveStatus.SAVED_SAMPLE).UID;
}
public override void AssertIdentical(Client expected, Client actual) {
base.AssertIdentical(expected, actual);
Assert.AreEqual(expected.FirstName, actual.FirstName,
"FirstName does not match");
Assert.AreEqual(expected.MiddleName, actual.MiddleName,
"MiddleName does not match");
Assert.AreEqual(expected.LastName, actual.LastName,
"LastName does not match");
Assert.AreEqual(expected.AddressUID, actual.AddressUID,
"AddressUID does not match");
}
}
ClientTest的样品发电机使用AddressTest.GetSample()创建一个虚拟地址时,填写一个虚拟的样本客户机。在这种类型的测试套件,这是一般的模式是经常使用的。任何测试需要一个虚拟的对象,只是简单地调用相应的样品发电机。
运行测试时,看起来与属性标记的任何类的NUnit的[TestFixture()]。它创建了一个类的实例并运行属性任何标记方法[试验(+)]。 [ExpectedException()]属性告诉NUnit的,给定的方法应该抛出异常。测试代码本身使用NUnit的的断言对象,以验证预期的属性。
从一个抽象基类继承的任何测试夹具也"继承"[{A6}]任何的测试方法。因此,AddressTest,一个具体的测试夹具,继承SaveAndFetch(),EditAndFetch(),和Delete()从PersistentObjectTest测试方法。请注意,派生类可以覆盖这些测试方法,例如,如果其相应的BLL类不支持删除:
继承[Test()]
public override void Delete() {
Assert.Ignore("This object does not support deleting");
}
现在,我们已经基本测试套件实施,说需求的变化,我们需要添加一个类代表一个首选的客户端收到折扣和特别信贷。首先,我们将创建一个PreferredClient从客户端的类:public class PreferredClient : Client {
private double _discountRate = 1;
private decimal _accountCredit = 0.00M;
//...
public override void Save() {
base.Save();
// call DAL to save this object's fields
}
//...
}
接下来,我们必须创建一个PreferredClientTest ClientTest派生的测试夹具。但是,这会导致一个问题:从PersistentObjectTestlt ClientTest继承; Clientgt;,但我们需要PreferredClientTest从PersistentObjectTestlt间接继承; PreferredClientgt; PersistentObjectTest的方法使使用正确类型的对象。该解决方案是移动"上下层次"ClientTest通用的签名:/// <summary>
/// Generic tester for classes derived from Client
/// </summary><
/span>
public class ClientTest<DerivedClientType>
: PersistentObjectTest<DerivedClientType>
where DerivedClientType : Client, new() {
public override void FillSample(DerivedClientType sample) {
base.FillSample(sample);
sample.FirstName = "FIRST" + DateTime.Now.Ticks.ToString();
sample.MiddleName = "MIDDLE" + DateTime.Now.Ticks.ToString();
sample.LastName = "LAST" + DateTime.Now.Ticks.ToString();
sample.AddressUID = new AddressTest().GetSample
(SampleSaveStatus.SAVED_SAMPLE).UID;
}
public override void AssertIdentical
(DerivedClientType expected, DerivedClientType actual) {
base.AssertIdentical(expected, actual);
Assert.AreEqual(expected.FirstName, actual.FirstName,
"FirstName does not match");
Assert.AreEqual(expected.MiddleName, actual.MiddleName,
"MiddleName does not match");
Assert.AreEqual(expected.LastName, actual.LastName,
"LastName does not match");
Assert.AreEqual(expected.AddressUID, actual.AddressUID,
"AddressUID does not match");
}
}
但是,我们需要保持非泛型测试仪,使客户的测试仍将运行:/// <summary>
/// Non-generic tester for base Client type
/// </summary><
/span>
[TestFixture()]
public class ClientTest : ClientTest<Client> {
// add Client-specific tests as needed
}
最后,我们在ClientTest通用版本定义PreferredClientTest:[TestFixture()]
public class PreferredClientTest : ClientTest<PreferredClient> {
public override void FillSample(PreferredClient sample) {
base.FillSample(sample);
Random r = new Random();
// some random dollars and cents
sample.AccountCredit = ((Decimal)r.Next()) + .25M;
sample.DiscountRate = r.NextDouble();
}
public override void AssertIdentical
(PreferredClient expected, PreferredClient actual) {
base.AssertIdentical(expected, actual);
Assert.AreEqual(expected.AccountCredit, actual.AccountCredit,
"AccountCredit does not match");
Assert.AreEqual(expected.DiscountRate, actual.DiscountRate,
"DiscountRate does not match");
}
}
注意FillSample()和AssertIdentical()方法简单地延长它们的基类的同行。人们可以很容易看到这种类型的扩展如何可以继续作为应用程序的增长,它是一个简单的添加一个子类,并实施适当的方法问题。缺点主键
这个假设的测试套件,一个明显的假设:它假定持久性对象是现实世界中的类的一个有效的基类。这种假设成为最明显的FETCH /填充方法,始终以一个长作为一个独特的数据库标识符。通常,一个真正的世界的数据库不会被标准化,这样所有的数据有一个bigint的主键(如果只!)。一个可以解决这个问题,通过扩大PersistentObjectTest和PersistentObject.Fetch通用()的签名,包括派生类的唯一标识符的类型。虚拟数据过载
由于其对样本发电机的依赖,测试套件的形式创建了一个虚拟的数据在数据库中的大量。这是可以接受的,因为一个测试数据库驱动的应用程序,核实数据的保存和检索正确的很大一部分。但是,这意味着,开发应用程序必须有一个专门的测试的数据库服务器,定期重置一些已知的状态,以防止虚拟数据盖过了有效的数据。此外,样本发生器的递归性质,可能使人们有可能进入一个永无止境的样本生成周期,可以非常快速地将一个数据库(更不用提的堆栈帧)一蹶不振。随机性
我所阐述的实现假定随机虚拟数据往往会足以满足大多数测试使用生成的对象。换句话说,消费者的样本对象必须确保生成的对象是否符合所需的先决条件。上随意性的界限,往往可以取得与参数,如以下示例发电机:/// <summary>
/// Return a client with one of the given first names
/// </summary><
/span>
/// <param name="firstNames">
/// The list of possible first names
/// </param>
public static Client GetBoundedSample
(string[] firstNames, SampleSaveStatus saveStatus) {
Client toReturn = new ClientTest().GetSample(SampleSaveStatus.UNSAVED_SAMPLE);
Random r = new Random();
toReturn.FirstName = firstNames[r.Next(0, firstNames.Length)];
if (saveStatus == SampleSaveStatus.SAVED_SAMPLE) {
toReturn.Save();
}
return toReturn;
}
然而,有没有一般情况下,容易实施的样品发电机的方式来控制的随意性或返回一个详尽的清单范围内的所有可能的样品。事实上,详尽的测试生成{A7}。结论
我所概述的是假设的测试套件架构,测试分层,合理,随机抽样数据往往需要数据库驱动的应用非常有用。通过使用测试夹具的继承和样品发电机,它变得非常容易扩大测试套件,作为应用程序的增长。它也减少了代码量需要测试一个数据库驱动的应用程序的最重要的方面:传输数据,并从数据库中正确。这个测试执行上的变化有好几个。几十到几千类NET应用程序。脚注在现实中,保存,填写,并删除通常会包装DoSave就会DoFill,DoDelete保护重写的方法。这将允许基类定义共同的前和后的数据库的操作步骤,同时离开的派生类来处理自己的数据。另外,删除通常会设置一个"忽略"的旗帜,而不是完全从数据库中删除数据。无论如何,我们在这篇文章中可以忽略这些并发症。只是假设,派生类会覆盖在明显的方式储存,填充和删除类的支持,如果相应的数据库操作。
这是不是真正的继承。 NUnit的使用{A8}找到属性[测试()]的方法在类的层次结构发生任何标记方法。此外,覆盖测试方法不保留[TEST()]属性。|布雷特丹尼尔