![](/img/trans.png)
[英]Should I use an implicit conversion to my return type, to aid in debugging?
[英]Should I use a callback or introduce a public field to aid with unit testing?
我已經看到添加了“單元測試的藝術” (由Roy Osherove提出)中倡導的公共屬性,以幫助建立SUT所使用的(或內部)協作者/協作的笨拙(或內部)協作者,並且我自己也使用了此技術以取得良好的效果。 (另外,我也看到了使用其他構造函數的類似方法)
同樣,測試隔離框架(例如Moq )可以提供替代方法,並且使用Moq,可以使用Callback來幫助建立笨拙的協作者/協作。
我在這里經歷的權衡是:
使用公共場所會在SUT中引入其他項目,並帶有稍微清晰的測試代碼
與
一個SUT不受其他項目的干擾,使其可測試,並且測試代碼稍顯笨拙(回調代碼不是最漂亮的)。
在我的情況下,由於什么是命令的約束並且應該是查詢(可能返回存根數據的查詢),所以沒有簡單的方法可以在SUT中管理協作( 沒有上述機制)
SUT中的協作者更新命令中通過引用傳遞的對象,這看起來很像:(稍后在代碼示例中重復)
var warehouse = new Warehouse(); // internal to Order
_repo.Load(warehouse); // Warehouse is filled by reference
編輯:我設計了一個存在設計問題的示例-倉庫和訂單過於親密,可以使用應用服務來安排交互等。問題的關鍵是,我對倉庫的填充方式幾乎沒有控制權。 我正在使用一個框架,該框架使用命令通過引用來水合對象。 我知道這是問題所在,但不幸的是我對此感到束縛。 因此,這個問題的真正重點不在於重新設計,而是如果我們要做的全部工作,則純粹是哪種方法,回調或公共字段將是更可取的。
下面的代碼示例都是使用Moq和NUnit的工作示例。 出於時間原因,我省略了添加應用程序服務來編排示例用例的步驟(該示例用例基本上是從符合要求的倉庫中填充訂單-基於Fowler's Mocks不是Stubs示例)。 同樣,這兩種方法都采用經典的單元測試方法,即斷言狀態而不是驗證行為,這不是我要解決的問題。
在繼續之前,我確實有一個偏好,但是我很想看看其他人的建議或偏愛。
因此,首先是公共財產的方法,代碼和測試:(在使用Func <>時很聰明)
public class Order
{
private readonly IWarehouseRepo _repo;
public int Items { get; private set; }
public Func<Warehouse> WarehouseBuilder { get; set; }
public Order(IWarehouseRepo repo)
{
_repo = repo;
}
public void AddOrderItems(int numberOfItems)
{
var warehouse = WarehouseBuilder();
_repo.Load(warehouse);
warehouse.RemoveStock(numberOfItems);
Items += numberOfItems;
}
}
public class Warehouse
{
public int Items { get; set; }
public void RemoveStock(int numberOfItems)
{
Items -= numberOfItems;
}
}
[TestFixture]
public class Given_A_Warehouse_With_20_Items
{
private Order _order;
private Mock<IWarehouseRepo> _warehouseRepo;
private Warehouse _warehouse;
[SetUp]
public void When_An_Order_Is_Placed()
{
_warehouseRepo = new Mock<IWarehouseRepo>();
_warehouse = new Warehouse() { Items = 20 };
_order = new Order(_warehouseRepo.Object);
_order.WarehouseBuilder = () => _warehouse;
_order.AddOrderItems(5);
}
[Test]
public void Then_The_Order_Now_Has_5_Items()
{
Assert.That(_order.Items, Is.EqualTo(5));
}
[Test]
public void Then_The_Warehouse_Now_Has_15_Items()
{
Assert.That(_warehouse.Items, Is.EqualTo(15));
}
}
public interface IWarehouseRepo
{
void Load(Warehouse warehouse);
}
其次是回調方法,代碼和測試:(回調中的Smarts)
public class Order
{
private readonly IWarehouseRepo _repo;
public int Items { get; private set; }
public Order(IWarehouseRepo repo)
{
_repo = repo;
}
public void AddOrderItems(int numberOfItems)
{
var warehouse = new Warehouse();
_repo.Load(warehouse);
warehouse.RemoveStock(numberOfItems);
Items += numberOfItems;
}
}
public class Warehouse
{
public int Items { get; set; }
public void RemoveStock(int numberOfItems)
{
Items -= numberOfItems;
}
}
[TestFixture]
public class Given_A_Warehouse_With_20_Items
{
private Order _order;
private Mock<IWarehouseRepo> _warehouseRepo;
private Warehouse _warehouse;
[SetUp]
public void When_An_Order_Is_Placed()
{
_warehouseRepo = new Mock<IWarehouseRepo>();
_warehouseRepo.Setup(repo => repo.Load(It.IsAny<Warehouse>())).Callback<Warehouse>(warehouseArgument =>
{
warehouseArgument.Items = 20;
_warehouse = warehouseArgument;
}
);
_order = new Order(_warehouseRepo.Object);
_order.AddOrderItems(5);
}
[Test]
public void Then_The_Order_Now_Has_5_Items()
{
Assert.That(_order.Items, Is.EqualTo(5));
}
[Test]
public void Then_The_Warehouse_Now_Has_15_Items()
{
Assert.That(_warehouse.Items, Is.EqualTo(15));
}
}
public interface IWarehouseRepo
{
void Load(Warehouse warehouse);
}
當正確使用時,添加公共狀態以使測試更容易是一種有效的技術。 同樣,進行復雜的測試,同時保持生產代碼不變,也是完全有效的。 兩者都可能是錯誤的,因此第三個選擇是還要查看您的設計。 實際上,您選擇的選擇取決於多種因素。 當心任何陳述一種真實方法的人。
添加公共狀態很容易,因為它很容易,但是添加狀態很差,因為如果您不針對代碼編寫自動測試,那么添加公共狀態就不會存在。 通常,當您掌握一些舊代碼並且添加一些其他字段並不重要時,這才有意義。 如果將這些設置為只讀,則還可以限制這些字段的范圍。 有趣的是,在軟件中,這種技術並未得到應有的使用。 在硬件領域中,電路等在物理上仍附有測試組件。 這些一旦運出就永遠不會使用。
這往往是您看到的最常見的形式。 雜亂無章或復雜的測試,它們可以使測試正常工作。 這里的好處是,至少設計並沒有為了測試而與公共領域妥協,但是您注意到的缺點是測試有些復雜和丑陋。 您可以通過使用SUT構建器或簡單地重構測試來彌補這一點,例如提取幫助程序以隱藏更復雜的部分。 如果這樣做,至少可以保留干凈代碼和更干凈測試的好處。
遺憾的是,使用率最低的應用程序可以解決您的問題。 測試難寫? 然后,您的設計就可以進行改進。 如果對新代碼的測試需要公共狀態以使其易於測試? 然后,您的設計就可以進行改進。
你的例子
在您的情況下,選擇兩個弊端中的較小者是有意義的。 使用回調的復雜測試將是我個人的建議。 只需權衡上述優點和缺點即可。 是的,測試看起來很復雜,由於您使用的工具,它的語法有些粗糙,但這還不是末日。
如果您真的不能更改裝載方式(假設第三方依賴性或其他原因),則還有另一種選擇。 創建您自己的倉庫存儲庫,該存儲庫將隱藏命令方面並返回新的倉庫實例。 現在,您將獲得一個查詢,然后可以輕松進行存根查詢並避免上述問題。 可悲的是,這會引入一個新組件,因此您需要權衡一下。 如果您可以將組件更改為查詢,那么我建議您首先這樣做。
我認為Func
並不是真正的公共狀態。 這不是一個領域。 關於設計錯誤的事實,這是一個相當新穎的技巧。
兩者都不怎么樣。 當我在這里進行思考時,請多多包涵。 這看起來更像是設計問題。 我想起了我最近讀的一篇文章,其中介紹了該訂單中的膠水是多么新 ,不應將其自身與必須新建 Warehouse
實例或負責提供某種配置方式(SRP)結合在一起。 我最初考慮添加一個新的依賴項,例如工廠。
public interface IFactory<T> where T: class {
T Create();
}
但是盡管如此,那樣只會給班級增加更多麻煩。 我的想法集中在避免在SUT中引入其他項目。
問題是,...根據您的示例,SUT需要一種方法來創建倉庫並加載它,但在保持其相對精簡的同時不負責任。 然后我開始思考...管理倉庫的工作/職責是什么...
IWarehouseRepo向我跳了起來,然后我想起了在實體框架的IDbSet中看到的模式。
public interface IWarehouseRepo {
Warehouse Create();
void Load(Warehouse warehouse);
}
不能撼動我對問題的思考過多,最終得到這樣的感覺
//My job is to provide a loaded warehouse to those who want one.
public interface IWarehouseProvider {
Warehouse GetWarehouse();
}
它將提供一個已加載的倉庫供訂單使用。 首先是它真正想要的。
public class Order {
private readonly IWarehouseProvider provider;
public int Items { get; private set; }
public Order(IWarehouseProvider provider) {
this.provider = provider;
}
public void AddOrderItems(int numberOfItems) {
//get a pre-loaded warehouse
var warehouse = provider.GetWarehouse();
warehouse.RemoveStock(numberOfItems);
Items += numberOfItems;
}
}
訂單不必在意創建或加載倉庫。 它只希望倉庫完成訂單。 為什么事情太復雜了。 我們安排得很周到,現在您想變得固執。 (我離題)
[TestFixture]
public class Given_A_Warehouse_With_20_Items
{
private Order _order;
private Mock<IWarehouseProvider> _warehouseProvider;
private Warehouse _warehouse;
[SetUp]
public void When_An_Order_Is_Placed() {
_warehouse = new Warehouse() { Items = 20 };
_warehouseProvider = new Mock<IWarehouseProvider>();
_warehouseProvider.Setup(provider => provider.GetWarehouse()).Returns(_warehouse);
_order = new Order(_warehouseProvider.Object);
_order.AddOrderItems(5);
}
[Test]
public void Then_The_Order_Now_Has_5_Items() {
Assert.That(_order.Items, Is.EqualTo(5));
}
[Test]
public void Then_The_Warehouse_Now_Has_15_Items() {
Assert.That(_warehouse.Items, Is.EqualTo(15));
}
}
對我來說,最終點亮燈泡的事情是您進行測試。 我決定向后處理您要測試的內容。
給定有20件物品的倉庫
當然,思考過程可能有缺陷,但是Order類只想要一個已加載的Warehouse,而不必關心它是如何創建或加載的。 我可能會在泥漿中旋轉,因為在我看來,這仍然像是Factory模式。
編輯。 潛在的提供者可以看起來像這樣
public class DefaultWarehouseProvider : IWarehouseProvder {
private readonly IWarehouseRepo repo;
public DefaultWarehouseProvider(IWarehouseRepo repo) {
this.repo = repo;
}
public Warehouse GetWarehouse() {
var warehouse = new Warehouse
repo.Load(warehouse);
return warehouse;
}
}
這看起來像您以前的樣子嗎? 是的,是的。 現在的事情是,它被抽象到自己的域中,從而使家屬可以繼續執行任務,而不必擔心香腸的制作方式。 您可以隔離/隔離您的約束,以使它們不會四處傳播代碼臭味疾病。 :)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.