Java 如何测试一个方法和它的两个助手之间的交互作用?

Java 如何测试一个方法和它的两个助手之间的交互作用?,java,unit-testing,groovy,spock,Java,Unit Testing,Groovy,Spock,我的Java代码的结构如下: public MyClass { // some class variables ... private void process() { private MyObject obj; ... obj = createHelper(); ... messageHelper(obj, "One of several possible strings");

我的
Java
代码的结构如下:

public MyClass {
    // some class variables
    ...
    private void process() {
        private MyObject obj; 
        ...
        obj = createHelper();
        ...
        messageHelper(obj, "One of several possible strings");
        ...  
        messageHelper(obj, "Another call with a different string");  
        ...
    }

    private MyObject createHelper {
        MyObject obj = new MyObject();
        // some Setter calls
        ...
        return obj;
    }

    private void messageHelper (MyOject obj, String message) {
        ...
    }

}
我想测试,基于属性
obj
(我想指定),
messageHelper()
接收正确的字符串。换句话说,我需要控制一种方法的结果,并访问另一种方法的参数

我仍然对所有这些模仿/存根/间谍的东西感到非常不安

在我看来,我需要对
MyClass
stub
CreateHelper()
使用“手动”创建的对象进行
Spy
,但不确定拦截
messageHelper()
调用参数的目的是什么

我还注意到对使用间谍的警告:

使用此功能前请仔细考虑。改变一下可能更好 规范下的代码设计

那么,用什么合适的斯波克方式来完成这项任务呢

稍微重构代码:(5/5/14)


如果有人建议如何使用Spock进行测试,我将不胜感激。您提到的注意事项就在这里。可测试代码和良好设计之间有着很好的相关性(我建议看一下MichaelFeathers的这节课来了解原因)

使用spies往往是设计问题的先兆,因为它通常是由于无法使用常规的模拟和存根而产生的

从您的示例中很难预测,因为您显然使用了伪名称,但是
MyClass
类的设计似乎违反了单一责任原则(),因为它执行处理、创建和消息传递(3个责任)

如果您愿意更改设计,以便处理类(
MyClass
)只执行处理,那么您将提供另一个创建类(
MyObjectFactory
),以及另一个通过构造函数执行消息传递的类(
MyObjectMessager
),setter方法或依赖项注入

使用这种新设计,您可以创建正在测试的类的实例(
MyClass
),并将工厂类和消息传递类的模拟对象传递给它。然后你就可以验证你想要的任何东西

看看这个例子(使用Mockito):

下面是一个Spock示例(未经测试,使用前请仔细检查…):

如果你是一把锤子,每个问题都是一颗钉子 我想在这里把这个规则称为例外,并说有时候存根私有方法——需要间谍——既正确又有用

@埃坦法尔对函数的分析很有可能是准确的,95%的情况下是这样,但就像大多数事情一样——我相信——并不总是如此

这是为我们这些相信他们有一个例外,但得到通常的“代码气味”的论点

我的示例是一个复杂的参数验证器。考虑以下事项:

class Foo {
    def doThing(...args) {
        doThing_complexValidateArgs(args)
        // do things with args
    }

    def private doThing_complexValidateArgs(...args) {
        // ... * 20 lines of non-logic-related code that throws exceptions
    }
}
  • 将验证器放在它自己的类IMO中会使问题变得过于独立。(a
    FooMethodArgumentValidator
    class?)
  • 重构验证可以显著提高
    doThing()
    函数的可读性
  • doThing\u complexValidateArgs()
    不应公开
doThing()
函数受益于一个简单调用的可靠性
validateArgs(…)
,并维护封装

现在我需要确定的是,我已经在父函数中调用了该函数。我该怎么做?好吧-如果我错了,请纠正我-但为了做到这一点,我需要一个
Spy()

下面是我用于静态私有方法的实际示例:

@SuppressWarnings("GroovyAccessibility")
@ConfineMetaClassChanges(DateService) // stops a global GroovySpy from affecting other tests by reseting the metaclass once done.
void "isOverlapping calls validateAndNormaliseDateList() for both args" () {
    List list1 = [new Date(1L), new Date(2L)]
    List list2 = [new Date(2L), new Date(3L)]
    GroovySpy(DateService, global: true) // GroovySpy allows for global replacement. see `org.spockframework.mock.IMockConfiguration#isGlobal()`

    when:
    DateService.isOverlapping(list1, list2)

    then:
    1 * DateService.isOverlapping_validateAndNormaliseDateList('first', list1) // groovy 2.x currently allows private method calls
    1 * DateService.isOverlapping_validateAndNormaliseDateList('second', list2)
}

是的,我认为创建一个单独的
MessageService
类是一个好主意,该类对每种类型的消息都有一个单独的方法。我不相信我需要工厂,因为只要我添加必要的覆盖构造函数并传递它们所需的参数,
MyObject
就可以创建自己的实例。
messageOne()
messageTwo()
MyClass
方法做什么?他们只是调用
IMessageService
类型的成员变量的方法吗?顺便说一句,我完全没有注意到你想要一个Spock解决方案,在你回答这些问题后,我会尝试摆脱我的Spock技能,在Spock中写一个例子。对不起。忘记保存我的编辑。这些方法属于
MessageService
。现在它似乎是自描述性的。我添加了一个Spock示例。您的类仍然需要一些重构,因为此时它创建了它使用的所有实例(即
MyObject
MessageService
),而不是从外部获取它们。我写《斯波克》已经有一段时间了,所以请相应地对待它:-)
public class MyClassSpec extends Specification {
    def "check that the right messages are produced with the expected object"() {
        given:
        def messageService = Mock(IMessageService)
        def testedInstance = new MyClass()

        testedInstance.setMessageService(messageService)

        when:
        testedInstance.process()

        then:
        1 * messageService.produceMessageOne(_)
        1 * messageService.produceMessageTwo(_)
    }
}
class Foo {
    def doThing(...args) {
        doThing_complexValidateArgs(args)
        // do things with args
    }

    def private doThing_complexValidateArgs(...args) {
        // ... * 20 lines of non-logic-related code that throws exceptions
    }
}
class FooSpec extends Specification {
            class Foo {
        def doThing(...args) {
            doThing_controlTest(args)
            doThing_complexValidateArgs(*args)
            // do things with args
        }

        def doThing_controlTest(args) {
            // this is a test
        }

        def private doThing_complexValidateArgs(...args) {
            // ... * 20 lines of code
        }
    }

    void "doThing should call doThing_complexValidateArgs" () {
        def fooSpy = Spy(Foo)

        when:
        fooSpy.doThing(1, 2, 3)

        then:
        1 * fooSpy.doThing_controlTest([1,2,3]) // to prove to ya'll we got into the right method
        1 * fooSpy.invokeMethod('doThing_complexValidateArgs', [1, 2, 3]) // probably due to groovy weirdness, this is how we test this call
    }
}
@SuppressWarnings("GroovyAccessibility")
@ConfineMetaClassChanges(DateService) // stops a global GroovySpy from affecting other tests by reseting the metaclass once done.
void "isOverlapping calls validateAndNormaliseDateList() for both args" () {
    List list1 = [new Date(1L), new Date(2L)]
    List list2 = [new Date(2L), new Date(3L)]
    GroovySpy(DateService, global: true) // GroovySpy allows for global replacement. see `org.spockframework.mock.IMockConfiguration#isGlobal()`

    when:
    DateService.isOverlapping(list1, list2)

    then:
    1 * DateService.isOverlapping_validateAndNormaliseDateList('first', list1) // groovy 2.x currently allows private method calls
    1 * DateService.isOverlapping_validateAndNormaliseDateList('second', list2)
}