如何测试方法与其两个助手之间的交互?

时间:2014-05-02 20:07:28

标签: 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");
        ...  
        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()会收到正确的字符串。换句话说,我需要控制一个方法的结果,并且可以访问另一个方法的参数。

我对所有这些Mock / Stub / Spy的东西仍然非常不稳定。

在我看来,我需要在SpyMyClass stub CreateHelper()上使用"手动"创建了对象,但不确定截取messageHelper()的调用参数的内容。

我还注意到Wiki警告不要使用间谍:

  

使用此功能前请三思。改变可能会更好   规范下的代码设计。

那么适当的 Spocky 完成任务的方式是什么?

略有重构的代码: (5/5/14)

public MyClass {
    // some class variables
    private messageSevice = new messageService();
    ...
    private void process() {
        private MyObject obj; 
        ...
        obj = new MyObject(parameters ...);
        ...
        if (someCondition) {
            messageService.produceMessageOne(obj);
        }
        ... 
        if (otherCondition) { 
            messageService.produceMessageTwo(obj);  
        {
        ...
    }

}

public class MessageService implements IMessageService {

    private final static MSG_ONE = "...";
    private final static MSG_TWO = "...";
    ...

    public void produceMessageOne(MyObject obj) {
         produceMessage(obj, MSG_ONE);
         ...
    }
    public void produceMessageOne(MyObject obj) {
         produceMessage(obj, MSG_TWO);
    }

    private void produceMessage(MyObject obj, String message) {
         ...
    }    
}

如果有人建议用 Spock 测试它的方式,我将不胜感激。

2 个答案:

答案 0 :(得分:3)

你所指的谨慎是正确的。可测试代码与优秀设计之间存在非常好的相关性(我建议观看Michael Feathers的讲座,了解为什么http://www.youtube.com/watch?v=4cVZvoFGJTU)。

使用间谍往往是设计问题的先机,因为它通常来自于使用常规模拟和存根的不可能性。

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

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

使用这个新设计,您可以创建您正在测试的类的实例(MyObjectMessager),并传递工厂和消息类的模拟对象。然后,您就可以在两者上验证您想要的任何内容。

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

MyClass

这是一个Spock示例(未经测试,使用前仔细检查......):

public class MyClassTest {
    @Test
    public void testThatProcessingMessagesCorrectly() {
        MyObject object = mock(MyObject.class);
        MyObjectFactory factory = mock(MyObjectFactory.class);
        when(factory.createMyObject()).thenReturn(object);
        MyObjectMessager messager = mock(MyObjectMessager.class);

        MyClass processor = new MyClass(factory, messager);
        processor.process();

        verify(factory).createMyObject();
        verify(messager).message(EXPECTED_MESSAGE_1);
        verify(messager).message(EXPECTED_MESSAGE_2);
        ...
        verify(messager).message(EXPECTED_MESSAGE_N);
    }

    ...
}

答案 1 :(得分:0)

如果你是锤子,每个问题都是钉子

我想在这里调用异常规则,并说有时隐藏私人方法 - 需要间谍 - 可以是正确和有用的。

@eitanfar在他对函数的分析中很可能是准确的,95%的情况是这样的,但与大多数事情一样 - 我相信 - 并非总是如此。

这适用于我们这些认为自己有异常但却能获得通常的#34代码味道的人。参数。

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

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过分关注问题。 (一个FooMethodArgumentValidator班?)
  • 重构验证可以说明显提高了doThing()函数的可读性。
  • doThing_complexValidateArgs()不应公开

doThing()函数受益于简单调用validateArgs(...)的可恢复性并保持封装。

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

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)
}