通过Category对测试方法进行分类(也可以对测试类进行分类)。这样,我们就可以在Nunit中,根据测试的情况,选择要测试的分类,或Exclude(排除)不测试的分类。我们运行一下,看NUnit的绿灯是否都亮了?测试通过后,就可以接着实现另外的日志类了。
public class ErrorLogDecorator:LogDecorator,IFee
{
public ErrorLogDecorator(IFee decoratee):base(decoratee){}
public double SettleFee(double money, int records)
{
try
{
double result = Decoratee.SettleFee(money,records);
Console.WriteLine(”Settling operation succeed!”);
return result;
}
catch (Exception ex)
{
Console.WriteLine(”The error occured while settling the fee.”);
Console.WriteLine(”The error is ” + ex.Message);
return 0;
}
}
}
public class ImplLogDecorator:LogDecorator,IFee
{
public ImplLogDecorator(IFee decoratee):base(decoratee)
{}
public double SettleFee(double money, int records)
{
double result = Decoratee.SettleFee(money,records);
Console.WriteLine(”The StoreProcedure whick was invoked is SpSettleFee.”);
Console.WriteLine(”Data table is: UserFee, OnLineRecord.”);
return result;
}
}
当然每做一步改进后,都需要修改测试代码进行单元测试。最后的单元测试代码:
using System;
using NUnit.Framework;
using FeeManagement;
namespace UnitTest
{
[TestFixture]
public class TestFee
{
private IFee fee;
[SetUp]
public void Init()
{
fee = new Fee();
}
[Test]
[Category(”SettleWithoutLog”)]
public void Settle()
{
Assert.IsNotNull(fee);
Assert.AreEqual(6,fee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasicLog”)]
public void SettleBasicLog()
{
IFee basicLogFee = new BasicLogDecorator(fee);
Assert.IsNotNull(basicLogFee);
Assert.AreEqual(6,basicLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithErrorLog”)]
public void SettleErrorLog()
{
IFee errorLogFee = new ErrorLogDecorator(fee);
Assert.IsNotNull(errorLogFee);
Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithImplLog”)]
public void SettleImplLog()
{
IFee implLogFee = new ImplLogDecorator(fee);
Assert.IsNotNull(implLogFee);
Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasic&ErrorLog”)]
public void SettleBasicErrorLog()
{
IFee basicLogFee = new BasicLogDecorator(fee);
IFee errorLogFee = new ErrorLogDecorator(basicLogFee);
Assert.IsNotNull(basicLogFee);
Assert.IsNotNull(errorLogFee);
Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasic&ImplLog”)]
public void SettleBasicImplLog()
{
IFee basicLogFee = new BasicLogDecorator(fee);
IFee implLogFee = new ImplLogDecorator(basicLogFee);
Assert.IsNotNull(basicLogFee);
Assert.IsNotNull(implLogFee);
Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithAllLog”)]
public void SettleAllLog()
{
IFee basicLogFee = new BasicLogDecorator(fee);
IFee implLogFee = new ImplLogDecorator(basicLogFee);
IFee errorLogFee = new ErrorLogDecorator(implLogFee);
Assert.IsNotNull(basicLogFee);
Assert.IsNotNull(implLogFee);
Assert.IsNotNull(errorLogFee);
Assert.AreEqual(6, errorLogFee.SettleFee(2.0,3));
}
[TearDown]
public void Dispose()
{
/*—*/
}
}
}
由于每一步都严格进行了单元测试,所以,我们对代码的正确性充满了信心。这也是单元测试的重要性及必要性所在。
七、真的结束了吗
从测试代码中看出,目前的解决方案还存在一个问题,就是日志对象的创建。由于日志对象可能会根据不同的情况,组合成不同的对象。如果不采取相应的方法来解决对象创建的问题,可能会造成对象管理的混乱。因此,我们还有必要引入工厂模式,专门负责日志对象的创建。
我最初考虑在工厂方法中,将这些日志类型放到一个Type[]数组中,然后再通过反射的方式创建对象。然而,由于创建日志对象的组合会很麻烦,采用这样的设计,反而会有过度设计的嫌疑。(这也是我为什么在Decorator类中使用构造函数而非采用专门的方法来设置Decoratee对象的原因。)
所以,只需要直接根据日志的情况为其分别创建相关的工厂方法就可以了。
public class DecoratorFactory
{
private static IFee fee = new Fee();
public static IFee CreateFee()
{
return fee;
}
public static IFee CreateBasicLogFee()
{
IFee basicLog = new BasicLogDecorator(fee);
return basicLog;
}
public static IFee CreateErrorLogFee()
{
IFee errorLog = new ErrorLogDecorator(fee);
return errorLog;
}
public static IFee CreateImplLogFee()
{
IFee implLog = new ImplLogDecorator(fee);
return implLog;
}
public static IFee CreateBasicErrorLogFee()
{
IFee basicLog = new BasicLogDecorator(fee);
IFee errorLog = new ErrorLogDecorator(basicLog);
return errorLog;
}
public static IFee CreateBasicImplLogFee()
{
IFee basicLog = new BasicLogDecorator(fee);
IFee implLog = new ImplLogDecorator(basicLog);
return implLog;
}
public static IFee CreateAllLogFee()
{
IFee basicLog = new BasicLogDecorator(fee);
IFee implLog = new ImplLogDecorator(basicLog);
IFee errorLog = new ErrorLogDecorator(implLog);
return errorLog;
}
}
然后再修改NUnit的测试代码:
[TestFixture]
public class TestFee
{
private IFee fee;
[SetUp]
public void Init()
{
fee = DecoratorFactory.CreateFee();
}
[Test]
[Category(”SettleWithoutLog”)]
public void Settle()
{
Assert.IsNotNull(fee);
Assert.AreEqual(6,fee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasicLog”)]
public void SettleBasicLog()
{
IFee basicLogFee = DecoratorFactory.CreateBasicLogFee();
Assert.IsNotNull(basicLogFee);
Assert.AreEqual(6,basicLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithErrorLog”)]
public void SettleErrorLog()
{
IFee errorLogFee = DecoratorFactory.CreateErrorLogFee();
Assert.IsNotNull(errorLogFee);
Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
}
[Test]
[ Category(”SettleWithImplLog”)]
public void SettleImplLog()
{
IFee implLogFee = DecoratorFactory.CreateImplLogFee();
Assert.IsNotNull(implLogFee);
Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasic&ErrorLog”)]
public void SettleBasicErrorLog()
{
IFee log = DecoratorFactory.CreateBasicErrorLogFee();
Assert.IsNotNull(log);
Assert.AreEqual(6,log.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithBasic&ImplLog”)]
public void SettleBasicImplLog()
{
IFee log = DecoratorFactory.CreateBasicImplLogFee();
Assert.IsNotNull(log);
Assert.AreEqual(6,log.SettleFee(2.0,3));
}
[Test]
[Category(”SettleWithAllLog”)]
public void SettleAllLog()
{
IFee log = DecoratorFactory.CreateAllLogFee();
Assert.IsNotNull(log);
Assert.AreEqual(6,log.SettleFee(2.0,3));
}
[TearDown]
public void Dispose()
{
/*—*/
}
}
经过这么多阶段的修改和完善,目前看来解决方案已经比较完善了。如果在Fee类中还有其他的方法,需要日志功能,方法仍然大同小异。因为在C#中可以同时实现多个接口,如果实现其他接口的类也要增加该日志功能,则日志的Decorator类同时还要去实现这个新的接口。好处是,你只需要修改这些实现,而调用的代码,却不用作大的修改了。因为要求提供日志功能的需求可能会不断增加,但只要日志的种类不变,用作装饰功能的日志对象个数就不会改变。
自然,本文讨论日志功能是完全站在OOP的角度来考虑的。如果引入AOP的思想,将日志看作是一个方面(Aspect),那么对于客户而言,可能会更简单。但这已不是本文要讨论的问题了。