模型的四種血量分類:
失血模型:property都是 { get; set; }独旷,沒有任何業(yè)務(wù)邏輯。
貧血模型:比如property的get內(nèi)部有業(yè)務(wù)邏輯,業(yè)務(wù)邏輯由模型內(nèi)部數(shù)據(jù)即可計算得出糜值,不依賴于外部。
充血模型:比如property的get內(nèi)部有業(yè)務(wù)邏輯踪央,業(yè)務(wù)邏輯依賴于外部接口臀玄,比如持久化接口。
脹血模型:模型內(nèi)部直接實現(xiàn)所有依賴畅蹂,接口都不需要了健无。
模型和上下文場景
class Order
{
DateTime CreatedTime { get; }
List<Item> Items { get; }
bool IsAvailable
{
get
{
return Items.Count > 0;
}
}
}
- Order是貧血模型,沒有外部接口依賴液斜。
- Order的property IsAvailable是我們關(guān)注和重構(gòu)的核心累贤。
- Order的物理結(jié)構(gòu)不會變化,業(yè)務(wù)復雜性只在IsAvailable的邏輯規(guī)則上增長少漆。
- Order由OrderManage的GetOrder方法提供實例臼膏。
- IsAvailable被廣泛使用,如下所示各種consumer示损。
class OrderManager : IOrderManage
{
public Order GetOrder() {...}
}
class OrderConsumerA
{
IOrderManage OrderManager
public void ConsumerABusiness()
{
var isOrderAvailable = OrderManager.GetOrder().IsAvailable;
...
}
}
class OrderConsumerB
{
IOrderManage OrderManager
public void ConsumerBBusiness()
{
var isOrderAvailable = OrderManager.GetOrder().IsAvailable;
...
}
}
class OrderConsumerC
{
IOrderManage OrderManager
public void ConsumerCBusiness()
{
var isOrderAvailable = OrderManager.GetOrder().IsAvailable;
...
}
}
單元測試代碼
使用單元測試框架Mock
class OrderConsumerATests
{
Mock<IOrderManage> mockOrderManager
public void ConsumerABusiness()
{
mockOrderManager.Setup(x => x.GetOrder()).Returns(new Order { Items = new List<Item> { new Item() }});
...
}
}
Order是Model渗磅,沒有基于抽象定義。無法使用Mock框架構(gòu)造一個fake的IsAvailable實現(xiàn)去直接返回我們testcase期望的結(jié)果。因此需要在每個testcase內(nèi)構(gòu)建一個Order實例始鱼。
當業(yè)務(wù)邏輯簡單且模型結(jié)構(gòu)簡單時仔掸,那么在各個consumer單元測試中重復構(gòu)造整個對象并不費力,我們不覺得痛医清,還可以接受的起暮。
但是,IsAvailabled的業(yè)務(wù)邏輯變復雜時
class Order
{
bool IsAvailable
{
get
{
return Items.Count > 0 && (CreatedTime.AddDays(7) >= DateTime.Now || (Items.All(item => !item.HasInventory) && Items.Sum(item => item.Price) < MaximumPrice)) ;
}
}
}
復雜度上升会烙,導致構(gòu)造適合每個testcase的Order對象變得復雜负懦,IsAvailable內(nèi)部邏輯在每個testcase中都會被執(zhí)行一次。維護單元測試就變得越來越困難柏腻,這是痛點纸厉。
想法
- 單元測試只關(guān)心單元內(nèi)部的邏輯和實現(xiàn),不要有類似于集成測試的單元測試
- 單元測試準備數(shù)據(jù)的過程不要太繁瑣葫盼,寫單元測試不要被準備數(shù)據(jù)的過程所阻礙
- 給IsAvailable一個virtual關(guān)鍵字就可以使用Mock<Order>對象解決問題残腌,但是感覺只是為了解決問題而改動,我們需要更好的方案
現(xiàn)在贫导,如果是你負責在Order的IsAvailable上再增加業(yè)務(wù)邏輯抛猫,面對越來越難以維護的單元測試,你會怎么重構(gòu)代碼孩灯?你會怎樣繼續(xù)寫單元測試去覆蓋新增加的業(yè)務(wù)邏輯闺金?現(xiàn)在, 不妨停下來思考一下先.
OK峰档,已有的想法是败匹,
1. 縱向-提取抽象到父類
給Order 一個父類OrderBase,對應(yīng)的abstract/virtual 關(guān)鍵字體現(xiàn)抽象讥巡。接下來只要Mock的setup就好了掀亩,不必多說。
class OrderBase
{
virtual bool IsAvailable(Order order);
}
Order上方的父類就是對業(yè)務(wù)邏輯抽象結(jié)構(gòu)的體現(xiàn)欢顷。理論上講與virtual IsAvailable性質(zhì)一樣槽棍,但是在model整體上做到了面向?qū)ο螅嫦虺橄蟆?br> 這種抽象是對model本身結(jié)構(gòu)的抽象抬驴。
2. 橫向-提取邏輯到接口
另外一條思路就是提取新的接口IOrderService炼七,業(yè)務(wù)邏輯向接口實現(xiàn)中遷移。所以這種方法會讓模型流血布持,有反內(nèi)聚的嫌疑豌拙,但是是否適用得具體問題具體分析。
interface IOrderService
{
bool IsAvailable(Order order);
}
然后题暖,Consumer引入
class OrderConsumerA
{
IOrderManage OrderManager
IOrderService OrderService
public void ConsumerABusiness()
{
var isOrderAvailable = OrderService.IsAvailable(OrderManager.GetOrder());
...
}
}
或者更徹底一些按傅,直接由OrderService依賴IOrderManage捉超,Consumer只依賴OrderService。
這種抽象是對IsAvailable邏輯的抽象逞敷。
總之狂秦,業(yè)務(wù)邏輯遷移到了service灌侣。之后我們的單元測試問題可以使用Mock<IOrderService>解決推捐。