如果从头开始设计易于使用TDD的新语言,会怎么样?


9

在使用某些最常见的语言(Java,C#,Java等)时,当您想要完全TDD代码时,有时似乎与该语言存在分歧。

例如,在Java和C#中,您将要模拟类的任何依赖关系,大多数模拟框架将建议您模拟接口而不是类。这通常意味着您有多个接口且只有一个实现(这种效果更加明显,因为TDD会迫使您编写大量较小的类)。让您正确模拟具体类的解决方案可以做一些事情,例如更改编译器或覆盖类加载器等,这很讨厌。

那么,如果从头开始设计一种对TDD来说很棒的语言,它将是什么样?可能是某种语言级别的方式来描述依赖项(而不是将接口传递给构造函数),并且能够不明确地分离类的接口?



2
没有语言需要 TDD。TDD是一种有用的做法,Hickey的要点之一就是仅仅因为您进行测试并不意味着您可能会停止思考
Frank Shearar 2012年

测试驱动开发是关于正确使用内部和外部API的,并且需要事先这样做。因此,在Java中所有关于接口-实际的类副产品。

Answers:


6

许多年前,我提出了一个解决类似问题的原型。这是屏幕截图:

零按钮测试

想法是断言与代码本身内联,并且所有测试基本上都在每次击键时运行。因此,一旦通过测试,就会看到该方法变为绿色。


2
哈哈,太神奇了!我实际上很喜欢将测试与代码放在一起的想法。.NET中具有单独的程序集和用于单元测试的并行名称空间的程序非常繁琐(尽管有充分的理由)。这也使重构更加容易,因为移动代码会自动移动测试:P
Geoff 2012年

但是您是否想将测试留在那里?您会为生产代码启用它们吗?也许它们可能是#ifdef的C版本,否则我们正在研究代码大小/运行时命中率。
Mawg说恢复Monica 2015年

它纯粹是一个原型。如果要成为现实,那我们就不得不考虑性能和尺寸之类的东西,但是现在要担心这一点还为时过早,而且如果我们做到了这一点,那么选择遗漏的内容就不难了或者,如果需要,将断言放在编译代码之外。感谢您的关注。
卡尔·马纳斯特

5

它是动态类型,而不是静态类型。 然后,鸭子类型输入将执行与静态类型语言中的接口相同的工作。而且,其类可以在运行时进行修改,以便测试框架可以轻松地对现有类进行存根或模拟方法。Ruby是一种这样的语言。rspec是其针对TDD的首要测试框架。

动态打字如何帮助测试

使用动态类型,您可以通过简单地创建一个类,该类具有与您需要模拟的协作对象相同的接口(方法签名),从而创建模拟对象。例如,假设您有一些发送消息的类:

class MessageSender
  def send
    # Do something with a side effect
  end
end

假设我们有一个使用MessageSender实例的MessageSenderUser:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

注意这里依赖注入的使用,这是单元测试的主要内容。我们将回到这一点。

您希望测试MessageSenderUser#do_stuff呼叫是否发送两次。就像使用静态类型的语言一样,您可以创建一个模拟MessageSender来计算send调用次数。但是与静态类型的语言不同,您不需要接口类。您只需继续创建它:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

并在测试中使用它:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

就其本身而言,与静态类型的语言相比,动态类型的语言的“鸭子类型”在测试中并没有增加多少。但是,如果没有关闭类但可以在运行时进行修改,该怎么办?那是改变游戏规则的人。让我们看看如何。

如果您不必使用依赖注入使类可测试怎么办?

假设MessageSenderUser仅将使用MessageSender发送消息,并且您无需允许将MessageSender替换为其他类。在单个程序中,通常是这种情况。让我们重写MessageSenderUser,以便它简单地创建和使用MessageSender,而无需依赖项注入。

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

现在,MessageSenderUser更加易于使用:创建它的任何人都无需创建MessageSender即可使用。在这个简单的示例中,这看起来并没有太大的改进,但是现在想象一下,MessageSenderUser是在多个位置创建的,或者它具有三个依赖关系。现在,该系统具有大量传递实例,只是为了使单元测试满意,而不是因为它根本无法改善设计。

开放类使您无需依赖注入就可以进行测试

具有动态类型和开放类的语言的测试框架可以使TDD变得相当不错。这是针对MessageSenderUser的rspec测试的代码片段:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

这就是整个测试。如果MessageSenderUser#do_stuff没有MessageSender#send准确地调用两次,则此测试将失败。真正的MessageSender类永远不会被调用:我们告诉测试,每当有人尝试创建MessageSender时,他们应该获取我们的模拟MessageSender。无需依赖注入。

在如此简单的测试中做很多事情真是太好了。除非确实对您的设计有意义,否则不必使用依赖注入会更好。

但这与开放类有什么关系?请注意对的调用MessageSender.should_receive。编写MessageSender时,我们没有定义#should_receive,所以谁做的呢?答案是测试框架通过对系统类进行一些仔细的修改,能够使它看起来像#should_receive通过在每个对象上定义的一样。如果您认为修改这样的系统类需要谨慎,那是对的。但这对于测试库在这里所做的事情来说是完美的,开放类使之成为可能。


好答案!你们开始谈论动态语言:)我认为鸭子输入是关键,.new的窍门也可能是静态类型的语言(尽管它不太优雅)。
杰夫2012年

3

那么,如果从头开始设计一种对TDD来说很棒的语言,它将是什么样?

“与TDD配合良好”肯定不足以描述一种语言,因此它可以“看起来”像任何东西。Lisp,Prolog,C ++,Ruby,Python ...任您选择。

此外,尚不清楚支持TDD是否由语言本身最好地处理。当然,您可以创建一种语言,其中每个函数或方法都具有关联的测试,并且可以建立对发现和执行这些测试的支持。但是,单元测试框架已经很好地处理了发现和执行部分,很难看到如何为每个功能清楚地添加测试的要求。测试还需要测试吗?还是有两类功能-需要测试的普通功能和不需要它们的测试功能?那看起来并不优雅。

也许最好通过工具和框架来支持TDD。将其构建到IDE中。创建一个鼓励它的开发过程。

另外,如果您正在设计一种语言,则最好长期思考。请记住,TDD只是一种方法,并不是每个人都喜欢的工作方式。可能难以想象,但是可能会有更好的方法来临。作为语言设计师,您是否希望人们在发生这种情况时不得不放弃您的语言?

您真正可以回答的唯一问题是,这种语言将有助于测试。我知道这没有多大帮助,但我认为问题出在问题上。


同意,这是很难表达的非常困难的问题。我认为我的意思是说,当前针对Java / C#等语言的测试工具感觉该语言有点阻碍,并且某些其他/替代语言功能将使整个体验变得更加优雅(即没有90的接口) %的类,只有从更高级别的设计角度出发才有意义的类)。
杰夫2012年

0

好吧,动态类型的语言不需要显式的接口。参见Ruby或PHP等。

另一方面,诸如Java和C#或C ++之类的静态类型语言会强制执行类型并迫使您编写这些接口。

我不明白的是您对他们有什么问题。接口是设计的关键要素,并且在整个设计模式中使用,并遵循SOLID原则。例如,我经常在PHP中使用接口,因为它们使设计明确,并且也可以强制执行设计。另一方面,在Ruby中,您无法强制执行类型,它是一种鸭子型语言。但是,您仍然必须想象那里的接口,并且必须在脑海中抽象设计以正确实现它。

因此,尽管您的问题听起来很有趣,但它暗示您在理解或应用依赖项注入技术方面存在问题。

为了直接回答您的问题,Ruby和PHP具有出色的模拟基础结构,它们都内置在它们的单元测试框架中,并且分别提供(请参阅Mockery for PHP)。在某些情况下,这些框架甚至允许您执行建议的操作,例如模拟静态调用或对象初始化,而无需显式注入依赖项。


1
我同意界面很棒,并且是关键的设计元素。但是,在我的代码中,我发现90%的类都有一个接口,并且该接口只有两种实现,即该类的类和模拟。尽管从技术上讲这只是接口的要点,但我不禁觉得它很不雅致。
杰夫2012年

我对Java和C#中的模拟不是很熟悉,但是据我所知,模拟对象模仿了真实对象。我经常通过使用对象类型的参数并将模拟发送给方法/类来进行依赖注入。类似于函数someName(AnotherClass $ object = null){$ this-> anotherObject = $ object吗?:新的AnotherClass; 这是不依赖接口而注入依赖关系的常用技巧。
Patkos Csaba

1
就我的问题而言,这绝对是动态语言相对于Java / C#类型语言具有优势的地方。一个具体类的典型模拟实际上会创建该类的子类,这意味着将调用具体类的构造函数,这是您绝对要避免的事情(有例外,但是它们有自己的问题)。动态模拟只利用鸭子类型,因此具体类与模拟之间没有关系。我曾经使用Python进行过很多编码,但是那是在TDD时代之前,也许是时候再看看!
杰夫2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.