2009-10-01 100 views
14

我目前正在参与使用C#进行开发 - 这里有一些背景: 我们使用我们的客户端应用程序实现了MVP,我们有一个圈规定,没有任何方法的圈复杂度应大于5. 这导致了很多小型的私人方法,这些方法通常对一件事负责。单元测试专用代码

我的问题是关于单元测试类:

测试通过的公共方法私有实现是所有罚款...我没有贯彻这是一个问题。

但是......怎么样了以下情况:

例1处理异步数据retrival请求的结果(回调方法不应该纯粹是为了测试是公开的)

例2.的事件,其确实的操作(如更新查看标签的文本 - 傻的例子,我知道...)

例3您正在使用第三方框架,该框架允许您通过覆盖受保护的虚拟方法来进行扩展(从公共方法到这些虚拟方法的路径通常被视为黑盒编程,并且将具有框架提供的各种依赖关系不想知道)

上面的例子在我看来不是由于糟糕的设计。 他们似乎也不会成为单独进行单独测试的候选人,因为这样的方法会失去其背景。

对此有没有人有任何想法?

干杯, 杰森

编辑: 我不认为我是在我原来的问题很清楚 - 我可以测试使用访问私有方法和使用TypeMock模拟出呼叫/方法。这不是问题。问题在于测试不需要公开的东西,或不能公开的东西。

我不想为了测试而公开代码,因为它可能引入安全漏洞(只发布一个接口来隐藏这个不是一个选项,因为任何人都可以将对象转换回原始类型并获得访问的东西,我不希望他们)

重构到另一个类进行测试的代码很好 - 但可能会丢失上下文。我一直认为有一个不好的习惯,那就是有'辅助'类,它可以包含一堆没有特定上下文的代码 - (在这里思考SRP)。我真的不认为这也适用于事件处理程序。

我很高兴被证明是错误的 - 我只是不确定如何测试这个功能!我一直认为,如果它可以打破或改变 - 测试它。

干杯,杰森

+0

你将使用什么隔离框架? – 2009-10-01 21:01:38

+0

我正在使用Typemock隔离器。嘲笑方法调用很容易 - 我真的不喜欢这样做,因为它会改变被测试类的行为! – Jason 2009-10-03 10:25:51

+1

所有这些让我希望我可以从头开始写自己的框架,所以我可以设计这些问题开始! – Jason 2009-10-03 11:02:13

回答

3

在我所有的单元测试,我从来没有打扰测试private功能。我通常只测试public函数。这与黑盒测试方法一致。

你是正确的,你真的,除非你暴露私有类无法测试私有函数。

如果你的“测试独立的类”是在同一个组件,您可以选择使用内部,而不是私人的。这将内部方法暴露给您的代码,但他们的方法不会在您的程序集中访问代码。

编辑:搜索这个主题我遇到了this question。答案最多的答案与我的答复类似。

+2

啊,但你实际上可以测试私人课程:)。应该在“但我应该吗?”下提交,但实际上可能。并且有些情况下我发现它很有用。 – Matt 2009-10-01 21:26:51

+0

+1 Matt:同上。 – 2009-10-02 02:01:54

0

我会承认,当最近写单元测试的C#我发现很多我知道Java的招数并没有真正应用(在我的情况下,它是测试的内部类)。

例如1,如果你能假/嘲笑数据检索处理程序,你可以得到通过伪造的访问回调。 (我知道大多数使用回调的其他语言也往往不会使它们变为私有的)。

例如2我会查看触发事件来测试处理程序。

示例3是其他语言中存在的模板模式示例。我已经看到了两种方法可以做到这一点:

  1. 测试全班反正(或至少它的相关件)。这在抽象基类自带测试或整体类不太复杂的情况下尤其适用。例如,在Java中我可能会这样做,如果我正在编写AbstractList的扩展。如果模板模式是通过重构生成的,则也可能是这种情况。

  2. 额外挂钩,允许直接调用保护方法再次扩展类。

13

正如克里斯所说的,标准做法是仅对公共方法进行单元测试。这是因为,作为该对象的消费者,您只关心公共可用的内容。而且,从理论上讲,用边缘案例进行适当的单元测试将充分利用它们所拥有的所有私有方法依赖性。这就是说,我发现有几次直接对私有方法编写单元测试是非常有用的,并且通过单元测试解释一些更复杂的场景或边缘情况可能是最简单的遇到。

如果是这样,您仍然可以使用反射来调用私有方法。

MyClass obj = new MyClass(); 
MethodInfo methodInfo = obj.GetType().GetMethod("MethodName", BindingFlags.Instance | BindingFlags.NonPublic); 
object result = methodInfo.Invoke(obj, new object[] { "asdf", 1, 2 }); 
// assert your expected result against the one above 
+0

+1,这很漂亮。我从来不知道反思可以做到这一点。 – Chris 2009-10-01 21:26:03

+0

你也可以使用私人访问器和(可选)传递一个私有对象来包装你的真实实例,这是一个非常干净的事情。我认为这也可以防止你在签名时遇到任何反射问题(不要引用我的话 - 我从来没有这样做过!!) – Jason 2009-10-03 10:35:43

7

我们有一个圈规则,规定 没有方法应该有一个 圈复杂度大于5

我喜欢这个规则更大。

问题是私有方法是实现细节。他们可能会改变/重构。你想测试公共接口。

如果您拥有复杂逻辑的私有方法,请考虑将它们重构为单独的类。这也可以帮助保持圈复杂度下降。另一个选择是使方法内部并使用InternalsVisibleTo(在Chris的答案中的一个链接中提到)。

当您在私有方法中引用外部依赖项时,捕获往往会进入。在大多数情况下,您可以使用诸如Dependency Injection等技术来分离您的课程。对于第三方框架的例子,这可能很困难。我会首先尝试重构设计以分离第三方依赖关系。如果这不可行,请考虑使用Typemock Isolator。我没有使用它,但它的主要特点是能够“模拟”私人,静态等方法。

类是黑匣子。以这种方式测试它们。

编辑:我会尝试回应杰森的评论我的答案和编辑原始问题。首先,我认为SRP更多的类,不离他们。是的,最好避免瑞士军队的助手课程。但是如何设计一个类来处理异步操作呢?还是数据检索课?原始课程的责任是这些部分,还是可以分开?

例如,假设您将此逻辑移至另一个类(可能是内部的)。该类实现Asynchronous Design Pattern,允许调用方选择是同步还是异步调用该方法。单元测试或集成测试是针对同步方法编写的。异步调用使用标准模式,复杂度低;我们不测试这些(除了在验收测试中)。如果异步类是内部的,使用InternalsVisibleTo来测试它。

+2

谢谢你的答案 - 我应该提到我使用TypeMock Isolator和嘲讽类,方法类不是问题。私人代码无法解决问题。什么是问题是(举例1)如果我嘲笑执行数据检索的类 - 我不能再调用回调函数。 (示例2)我无法测试私人事件处理程序,而无需深入到类的私有方法中。 – Jason 2009-10-03 10:39:30

4

实在是只有两个需要考虑的情况:

  1. 专用代码被调用时,直接或间接地从公共代码和
  2. 专用代码是公共代码调用。

在第一种情况下,专用代码会自动被其行使该调用私有代码市民代码测试的测试,因此没有必要测试专用代码。在第二种情况下,私人代码根本无法被调用,因此应删除,未经测试。

Ergo:没有必要明确地测试私有代码。

请注意,当你做TDD时,它是不可能为未经测试的私有代码甚至存在。因为当你执行TDD时,私有代码可以出现的唯一方式是通过Extract {Method | Class | ...}从公共代码重构。根据定义,重构是保持行为并因此保持测试覆盖。公开代码的唯一出现方式就是失败测试的结果。如果公共代码由于失败的测试而只能显示为已经测试过的代码,并且私有代码只能通过保留行为的重构从公共代码中提取出来,那么未经测试的私有代码永远不会出现。

+0

当你考虑我给出的第一个或第二个例子时,我不相信这是真的。您断言专用代码只能作为重构公共代码的直接结果存在 - 但这并不能解释如何测试事件处理程序或异步回调的代码。这些是我所遇到的真正的两个问题,无法让我的头脑! – Jason 2009-10-03 10:32:14

2

从TDD人谁已经在C#中被敲打周围的几个要点:

1)如果你面向接口编程,然后一类是不是在接口的任何方法是有效的私有。您可能会发现这是提升可测试性的更好方法,也是更好的使用接口的方法。测试这些公众成员。

2)这些小帮手方法可能更适合属于某个其他类。寻找功能羡慕。作为原始类的私人成员(你发现它)可能是不合理的,它可能是它所羡慕的类的合理的公共方法。在新班级中作为公众成员进行测试。

3)如果你检查一些小的私有方法,你可能会发现它们具有凝聚力。它们可能代表了与原始类别不同的​​较小类别的兴趣。如果是这样,那个类可以拥有所有的公共方法,但是可以作为原始类的私有成员,或者可以在函数中创建和销毁。在新班级中作为公众成员进行测试。

4)您可以从原始派生一个“Testable”类,在这个类中创建一个新的公用方法,除了调用旧的私有方法外什么也不做。可测试类是测试框架的一部分,而不是生产代码的一部分,所以对它有特殊的访问权限是很酷的。在测试框架中进行测试,就好像它是公开的。

所有这些都使得对当前使用私有助手方法的方法进行测试变得非常简单,而不会影响智能感知的工作方式。

0

不要测试私人代码,否则在稍后重构时会后悔。然后,你会像Joel一样做博客,关于TDD是如何工作太多的,因为你不得不用代码重构你的测试。

有技术(嘲笑,存根)做适当的黑匣子测试。看看他们。

1

有几个人回应说,不应该直接测试私有方法,或者应该将其移至另一个类。虽然我认为这很好,但有时它不值得。虽然我原则上同意这一点,但我发现这是为了节省时间而不会产生负面影响的那些规则之一。如果函数很小/简单,那么创建另一个类和测试类的开销就会过大。我会公开这些私有方法,但不会将它们添加到接口中。这种方式的消费者(谁应该只通过我的IoC库获得接口)不会意外使用它们,但它们可用于测试。

现在在回调的情况下,这是一个很好的例子,其中公开私有属性可以使测试更容易编写和维护。例如,如果A类将回调传递给B类,我将使该回调成为A类的公共属性.A类的一个测试使用B的存根实现来记录传入的回调。然后,测试将验证在适当的条件下回调被传递给B.然后,A类的单独测试可以直接调用回调函数,验证它具有适当的副作用。

我认为这种方法非常适用于验证异步行为,我一直在做一些JavaScript测试和一些lua测试。好处是我有两个小的简单测试(验证回调的设置,验证它的行为如预期)。如果您试图保持回调私密,那么验证回调行为的测试有更多的设置要做,并且该设置将与应该在其他测试中的行为重叠。糟糕的联结。

我知道,它不漂亮,但我认为它运作良好。

+0

对于你的第一个建议 - 我会不愿意这样做,因为你只需要简单的点击就可以在你的应用程序中引入一个巨大的安全保留。 您对处理回调的建议大概就像我使用异步方法测试一样......我真的结束了将代码更改回私人并更改我的测试以最终测试使用访问器的私有实现,因为我没有做到这一点,不想让代码公开。这样做只是有点“气味”:( – Jason 2009-10-03 10:43:07

+0

你可以扩展与铸造相关的安全漏洞吗? – 2009-10-05 00:19:14

+0

嗨,弗兰克,使用一点反思,它不是很难找出背后的实际类型一个接口,然后可以将实现该接口的对象转换回原来的类型,并访问通过接口无法访问的公共方法。据我所知,(在这种情况下)一个接口的点不是安全的 - 它是向开发者指示类(或多态)的正确用法 – Jason 2009-10-05 21:54:09

0

这是一个在引入测试时很早就出现的问题。解决此问题的最佳方法是黑盒测试(如上所述)并遵循单一责任原则。如果你的每个类只有一个理由要改变,他们应该很容易测试他们的行为,而不必使用他们的私有方法。

SRP - wikipedia/pdf

这也导致了更强大和适应性代码为单一职责原则实际上只是说,你的类应该具有高cohesion

+0

在这种情况下,您将如何解决示例1? – 2009-10-02 22:19:10

+0

我不得不看到代码,但马上我会说文件检索本身不需要知道它是异步的。您应该分别实现该功能并将其扩展为支持异步传输。在这种情况下,我假设您正在使用完整的异步请求并在回调中处理结果。这两者已经明确分开。一个发送,另一个接收。测试发送。测试接收。一起测试。更多的事情将会公开,但不在您现有的情况下。 – 2009-10-02 22:48:33

+0

这里是我如何实现你的建议(请纠正我,如果我错了!)我有一个类(C1),它可以以异步方式获取数据。我的调用类(C2)调用数据检索类。调用类不知道异步调用需要多长时间(因为它是在另一个线程上定义的)。数据检索类发出信号通知它已检索到数据。 C2然后从C1中提取数据。那么如何在不引入竞争条件的情况下对其进行全面测试?测试发送和测试接收是否正常 - 但我如何一起测试? – Jason 2009-10-03 10:51:24

2

这里有一些很好的答案,我基本上同意重复建议发芽新班。对于示例3,然而,有一个偷偷摸摸的,简单的方法:

例3.您使用的是第三方 框架,它可以让你通过重写受保护的虚拟 方法(路径从公共 延长 这些虚拟方法方法 通常被视为黑盒 编程,将有各种 依赖关系框架 提供你不想知道 约)

假设MyClass扩展FrameworkClass。让MyTestableClass扩展MyClass,然后在MyTestableClass中提供公共方法,公开您需要的MyClass的受保护方法。这不是一个很好的做法 - 这对于糟糕的设计是一种促成因素 - 但有时很有用,而且非常简单。

0

在C#中,你可以在AssemblyInfo.cs中使用属性:

[assembly: InternalsVisibleTo("Worker.Tests")] 

只需使用内部标记您的私有方法,测试项目将仍然可以看到该方法。简单!你可以保持封装并进行测试,而不需要所有的TDD废话。

+0

下次,请将问题标记为重复,而不是发布多个相同的答案。谢谢! – Rob 2016-12-29 05:26:06