【问题标题】:How to test the interaction between a method and its two helpers?如何测试一个方法和它的两个助手之间的交互?
【发布时间】:2014-06-19 14:37:51
【问题描述】:

我的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 的东西感到非常不安。

在我看来,我需要在 MyClassstub CreateHelper() 上使用“手动”创建的对象来 Spy,并且不确定要拦截 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 进行测试的方式,我将不胜感激。

【问题讨论】:

    标签: java unit-testing groovy spock


    【解决方案1】:

    你提到的谨慎是正确的。可测试的代码和好的设计之间有很好的相关性(我建议观看 Michael Feathers 的讲座以了解为什么 http://www.youtube.com/watch?v=4cVZvoFGJTU)。

    使用间谍往往是设计问题的一个提示,因为它通常是由于无法使用常规模拟和存根而引起的。

    从您的示例中很难预测,因为您显然使用了伪名称,但似乎MyClass 类的设计违反了单一职责原则(http://en.wikipedia.org/wiki/Single_responsibility_principle),因为它确实处理,创建和消息传递(3 项职责)。

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

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

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

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

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

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

    【讨论】:

    • 是的,我认为创建一个单独的MessageService 类是个好主意,该类将为每种类型的消息提供一个单独的方法。我不相信我需要一个工厂,因为MyObject 完全有能力创建自己的实例,只要我添加必要的覆盖构造函数并传递给它们所需的参数。
    • MyClassmessageOne()messageTwo() 方法是做什么的?他们只是调用IMessageService 类型的成员变量的方法吗?顺便说一句,我完全没有注意到您想要一个 Spock 解决方案,在您回答这些问题后,我将尝试刷掉我的 Spock 技能并在 Spock 中编写一个示例。
    • 对不起。忘记保存我的编辑。这些方法属于MessageService
    • 现在它似乎是自我描述的。
    • 我添加了一个 Spock 示例。你的类仍然需要一些重构,因为此时它创建了它使用的所有实例(即MyObjectMessageService),而不是从外部获取它们。自从我写 Spock 以来已经有一段时间了,所以请相应地对待它:-)
    【解决方案2】:

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

    我想在这里调用异常规则并说有时存根私有方法(需要间谍)可能既正确又有用。

    @eitanfar 对函数的分析很可能是准确的,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 类中会过多地分离问题。 (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)
    }
    

    【讨论】:

      猜你喜欢
      • 2013-12-31
      • 1970-01-01
      • 1970-01-01
      • 2018-06-20
      • 1970-01-01
      • 1970-01-01
      • 2013-07-25
      • 2020-03-15
      • 2016-07-11
      相关资源
      最近更新 更多