TDD:存根、模拟或以上都不是

发布于 2024-07-11 20:15:29 字数 1445 浏览 8 评论 0原文

我正在尝试通过将 TDD 应用到我的一个简单项目中来学习它。 一些详细信息(以及之前的问题)位于:

TDD:帮助编写可测试类

具体情况是我有一个PurchaseOrderCollection 类,它有一个PurchaseOrders 的私有列表(在构造函数中传递),并且PurchaseOrders 有一个布尔属性IsValid。 PurchaseOrderCollection 有一个属性 HasErrors,如果列表中的任何 PurchaseOrders 的 IsValid 为 false,则该属性返回 true。 这就是我想测试的逻辑。

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{
    List<PurchaseOrder> orders = new List<PurchaseOrder>();

    orders.Add(new PurchaseOrder(--some values to generate IsValid false--));
    orders.Add(new PurchaseOrder(--some values to generate IsValid true--));

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

这与我之前的问题类似,因为这个测试过于耦合,因为我必须知道使 PurchasingOrder IsValid 为 false 或 true 的逻辑才能通过测试,而实际上这个测试不应该关心。 问题是不同的(imo),因为类本身不是问题。

本质上,我希望能够声明 IsValid 为 false 或 true 的 PurchasingOrder,而无需了解有关 PurchaseOrder 是什么的更多信息。

根据我有限的 TDD 知识,这是您使用存根或模拟的目的。 我的主要问题是,这是正确的吗? 或者我应该为此使用不同的方法? 或者我完全有缺陷,只是写了这个测试并思考了错误?

我最初的想法是使用某种模拟框架并创建一个始终返回 true 或 false 的 PurchaseOrder 。 但根据我读到的内容,我需要声明 IsValid 虚拟。 所以我的第二个想法是更改它以添加 IPurchaseOrder 作为 PurchaseOrder 的接口,并创建一个始终返回 false 或 true 的假的 PurchaseOrder 。 这两种想法都有效吗?

谢谢!

I'm trying to learn TDD by applying it to a simple project of mine. Some details (and an earlier question) are here:

TDD: Help with writing Testable Class

The specifics are I have a PurchaseOrderCollection class that has a private List of PurchaseOrders (passed in at constructor), and the PurchaseOrders have a boolean property IsValid. The PurchaseOrderCollection has a property HasErrors that returns a true if any PurchaseOrders in the list have IsValid as false. This is the logic I want to test.

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{
    List<PurchaseOrder> orders = new List<PurchaseOrder>();

    orders.Add(new PurchaseOrder(--some values to generate IsValid false--));
    orders.Add(new PurchaseOrder(--some values to generate IsValid true--));

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

This is similar to my previous question in that this test is too coupled in that I have to know the logic of what makes a PurchaseOrder IsValid false or true to pass the test, when really this test shouldn't care. The question is different (imo) in that the classes themselves aren't the problem.

Essentially I want to be able to declare a PurchaseOrder that has IsValid false or true without knowing anything more about what a PurchaseOrder is.

From my limited TDD knowledge, this is something you use Stubs or Mocks for. My main question, is this correct? Or should I be using a different method for this? Or am I completely flawed and am just writing this test and thinking about it wrong?

My initial thought was to just use some kind of mock framework and create a PurchaseOrder that always returns true or false. From what I've read though, I'd need to declare IsValid virtual. So my second thought was to change it to add IPurchaseOrder as an interface for PurchaseOrder and just create a fake PurchaseOrder that always returns false or true. Are both of these valid ideas?

Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

远山浅 2024-07-18 20:15:30

无论是创建存根还是模拟,您都走在正确的轨道上。 我更喜欢使用 Mocking 框架。

使用模拟框架的工作方式是,您希望模拟您的PurchaseOrder 类,从而抽象出它的实现。 然后设置调用 IsValid 的期望,并在调用时返回该值。

如果您使用的是 C# 3.0 和 .NET Framework 3.5,则使用 Moq 的示例:

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{    
    var mockFirstPurchaseOrder = new Mock<IPurchaseOrder>();
    var mockSecondPurchaseOrder = new Mock<IPurchaseOrder>();

    mockFirstPurchaseOrder.Expect(p => p.IsValid).Returns(false).AtMostOnce();
    mockSecondPurchaseOrder.Expect(p => p.IsValid).Returns(true).AtMostOnce();

    List<IPurchaseOrder> purchaseOrders = new List<IPurchaseOrder>();
    purchaseOrders.Add(mockFirstPurchaseOrder.Object);
    purchaseOrders.Add(mockSecondPurchaseOrder.Object);

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

编辑:
这里我使用了一个接口来创建PurchaseOrder 的模拟,但您也没有。 您可以将 IsValid 标记为虚拟并模拟 PurchaseOrder 类。 我的经验法则是首先使用虚拟。 只是为了创建一个接口,这样我就可以在没有任何架构原因的情况下模拟一个对象,这对我来说是一种代码味道。

You are on the right track with either creating a stub or a mock. I prefer using a Mocking framework.

How it would work using a mocking framework is that you would want to mock your PurchaseOrder class therefore abstracting away it's implementation. Then setup expectations that IsValid is called and when it is called return this value.

Example using Moq, if you're using C# 3.0 and .NET Framework 3.5:

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{    
    var mockFirstPurchaseOrder = new Mock<IPurchaseOrder>();
    var mockSecondPurchaseOrder = new Mock<IPurchaseOrder>();

    mockFirstPurchaseOrder.Expect(p => p.IsValid).Returns(false).AtMostOnce();
    mockSecondPurchaseOrder.Expect(p => p.IsValid).Returns(true).AtMostOnce();

    List<IPurchaseOrder> purchaseOrders = new List<IPurchaseOrder>();
    purchaseOrders.Add(mockFirstPurchaseOrder.Object);
    purchaseOrders.Add(mockSecondPurchaseOrder.Object);

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

Edit:
Here I used an interface to create the mock of PurchaseOrder, but you don't have too. You could mark IsValid as virtual and mock the PurchaseOrder class. My rule of thumb when which way to go is to use virtual first. Just to create an interface, so I can mock an object without any architectual reason is a code smell to me.

转瞬即逝 2024-07-18 20:15:30

...这个测试太耦合了,因为我
必须知道是什么构成的逻辑
PurchaseOrder IsValid 为 false 或 true
通过测试,当真的这个测试
不应该在意...

我实际上会反驳——对于你的测试来说,知道有效性被建模为采购订单中的布尔值意味着你的测试对PurchaseOrder的实现了解太多(假设它实际上是PurchasingOrderCollection 的测试)。 我对使用现实世界的知识(即有效或无效的实际值)创建适当的测试对象没有问题。 最终,这确实是您正在测试的内容(如果我给我的收藏提供了具有荒谬值的采购订单,它会正确地告诉我存在错误吗)。

一般来说,我会尽量避免为“实体”对象(例如PurchaseOrder)编写接口,除非除了测试之外还有其他原因这样做(例如,生产中有多种PurchaseOrders,而接口是对其进行建模的最佳方式) )。

当测试表明您的生产代码可以更好地设计时,这真是太好了。 不过,仅仅更改生产代码来使测试成为可能并不是很好。

好像我写得还不够,这是另一个建议——这就是我在现实生活中实际解决这个问题的方法。

创建一个具有接口的PurchaseOrderValidityChecker。 设置 isValid 布尔值时使用它。 现在创建有效性检查器的测试版本,让您指定给出哪个答案。 (请注意,此解决方案可能还需要一个PurchaseOrderFactory 或等效项来创建PurchaseOrders,以便每个采购订单在创建时都可以得到对PurchaseOrderValidityChecker 的引用。)

...this test is too coupled in that I
have to know the logic of what makes a
PurchaseOrder IsValid false or true to
pass the test, when really this test
shouldn't care...

I'd actually argue the reverse -- that for your test to know that validity is modeled as a boolean within the purchase order means that your test knows too much about the implementation of PurchaseOrder (given that it's actually a test of PurchaseOrderCollection). I don't have a problem with using real-world knowledge (i.e. actual values that would be valid or invalid) to create appropriate test objects. Ultimately, that's really what you're testing (if I give my collection a purchase order with ridiculous values, will it correctly tell me there are errors).

In general I try to avoid writing an interface for an "entity" object such as a PurchaseOrder unless there's some reason to do so other than for testing (e.g. there are multiple kinds of PurchaseOrders in production and an interface is the best way to model that).

It's great when testing reveals that your production code could be better designed. It's not so good, though, to change your production code just to make a test possible.

As if I haven't written enough, here's another suggestion -- and this is the way I would actually solve this in real life.

Create a PurchaseOrderValidityChecker that has an interface. Use that when setting the isValid boolean. Now create a test version of the validity checker that lets you specify which answer to give. (Note that this solution probably also requires a PurchaseOrderFactory or the equivalent for creating PurchaseOrders, so that each purchase order can be given a reference to the PurchaseOrderValidityChecker when it is created.)

何止钟意 2024-07-18 20:15:30

我最近问了一个关于测试的类似问题。 不要忘记这一点:做您需要做的最简单的事情,然后在必要时进行重构。 我个人尽量着眼于大局,但我也抵制过度设计解决方案的冲动。 您可以将两个 PurchaseOrder 字段添加到测试类中,其中一个有效,一个无效。 使用这些字段将您的PurchaseOrderCollection 置于您想要测试的状态。 您最终需要学习如何进行模拟,但在这种情况下,当普通锤子可以解决问题时,您不需要大锤。 通过使用模拟的PurchaseOrder 而不是处于您想要的状态的具体PurchaseOrder,您不会获得任何价值。

最重要的是,您可以从测试 PurchasingOrderCollection 的行为中获得更多收益,而不仅仅是测试 PurchasingOrderCollection 的状态。 在您的测试验证 PurchasingOrderCollection 可以进入其不同状态后,更重要的测试是行为测试。 通过您认为合适的任何方式将您的采购订单集合置于有效和无效状态(模拟或更新所需状态的具体类),并测试PurchaseOrderCollection每个状态的逻辑是否正确执行,而不仅仅是PurchaseOrderCollection只是处于有效/无效状态。

PurchaseOrderCollection 将始终依赖于另一个类,因为它是一个专门的集合。 知道 IPurchaseOrder 具有 IsValid 属性与知道具体的 PurchaseOrder 具有 IsValid 属性没有任何不同。 我会坚持使用最简单的有效方法,例如具体的采购订单,除非您有充分的理由相信您的系统中将有多种类型的采购订单。 那时,PurchaseOrder 接口就更有意义了。

I recently asked a somewhat similar question about testing. Don't forget this: do the simplest thing that you need to do, then refactor when necessary. I personally try to keep the bigger picture in mind, but I also resist the urge to overengineer my solutions. You could add two PurchaseOrder fields into your test class where one is valid and one is invalid. Use those fields to put your PurchaseOrderCollection into the state that you want to test. You will need to learn how to mock eventually, but in this case you don't need a sledgehammer when a regular hammer will solve the problem. You don't gain any value by using a mock PurchaseOrder instead of a concrete PurchaseOrder that's in the state that you want.

Most importantly, you gain much more from testing the behavior of PurchaseOrderCollection instead of just testing the state of your PurchaseOrderCollection. After your tests verify that the PurchaseOrderCollection can be put into its different states, then the more important tests are the behavioral tests. Put your purchase order collection into both a valid and invalid state by whatever means you feel is appropriate (mocking or newing up concrete classes in the desired state), and test that the logic for each state of PurchaseOrderCollection is properly executed and not just that PurchaseOrderCollection is simply in a valid/invalid state.

PurchaseOrderCollection will always have a dependency on another class since it's a specialized collection. Knowing that IPurchaseOrder has a IsValid property isn't any different than knowing that the concrete PurchaseOrder has a IsValid property. I'd stick with the simplest thing that works, e.g. a concrete PurchaseOrder, unless you have much reason to believe that you will have multiple types of PurchaseOrders in your system. At that point, a PurchaseOrder interface would make more sense.

流心雨 2024-07-18 20:15:30

我可能在这里缺少一些上下文,但在我看来,您必须以示例的方式“耦合”您的测试,否则您实际上并没有测试任何东西 (IsValid 属性除外,该属性很简单)。

模拟采购订单没有任何好处 - 您已经

使用存根测试了模拟,而不是真实的类 - 同样的事情

也可以 - 如果不是强制性的 - 使用 TDD 时进行白盒测试

i may be missing some context here, but it seems to me that you must 'couple' your test in the manner of your example, otherwise you aren't really testing anything (except an IsValid property, which is trivial).

mocking the purchase order gains nothing - you've tested the mock, not the real class

using a stub - same thing

it is okay - if not mandatory - to white-box test when using TDD

最冷一天 2024-07-18 20:15:30

首先,请记住您正在测试集合,而不是 PurchaseOrder,因此这就是您的努力所在。 这取决于 PurchaseOrder 的复杂程度。 如果它是一个具有明显行为的简单实体,那么创建实例可能是有意义的。 如果它更复杂,那么按照您的描述提取接口是有意义的。

提出的下一个问题是该界面中有什么。 集合中的对象需要扮演什么角色? 也许您只需要知道它们是否有效,在这种情况下,您可以提取 IValidatable 并缩小代码中的依赖关系。 我不知道这种情况下的真实情况是什么,但我经常发现我可以使用接口来推动我编写更集中的代码。

First, remember that you're testing the collection, not the PurchaseOrder, so that's where your effort goes. It depends on how complex PurchaseOrder is. If it's a simple entity with obvious behaviour, then it probably makes sense just to create instances. If it's more complex, then it makes sense to extract an interface as you described.

The next question that raises is what's in that interface. What is the role that objects in the collection need to perform? Maybe you only need to know if they're valid, in which case you can extract IValidatable and narrow the dependencies in the code. I don't know what's true in this case, but I often find that I can use interfaces to push me to more focussed code.

樱花落人离去 2024-07-18 20:15:30

我不是单元测试方面的专家,但这是我过去所做的。 如果您有一个可以有效/无效的 PurchaseOder 类,那么我确信您也对它们进行了单元测试,以查看它们是否确实有效。 为什么不调用这些方法来生成有效和无效的 PurchaseOrder 对象,然后将其添加到您的集合中?

I'm no expert in unit testing, but here's what I've done in the past. If you have a PurchaseOder class that can be valid/invalid, then I'm sure you also have unit tests for those, to see if they do in fact validate. Why not call those methods to generate valid and invalid PurchaseOrder objects, and then add that to your collection?

独闯女儿国 2024-07-18 20:15:30

这两个想法都有效吗?

是的。

您还可以创建一个可以返回有效和无效的采购订单的对象母体。

Are both of these valid ideas?

Yes.

You might also create an Object Mother that can return both valid and invalid PurchaseOrders.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文