簡體   English   中英

我應該使用回調還是引入公共字段來輔助單元測試?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM