spockframework / spock

The Enterprise-ready testing and specification framework.
https://spockframework.org
Apache License 2.0
3.56k stars 470 forks source link

Confusing Mocking and Verification #112

Closed xnslong closed 9 years ago

xnslong commented 9 years ago

I made some changes with a method, so I want to test the logic of this change. But I just feel it difficult to write a test function that reads clearly enough to explain my expectation. The problem is as following.

The method I am testing is foo(input), which will call an underlying method underlyingService.sampleMethod(argObject): Result. The changes I made will affect the argObject passed from the foo method, and what I want to test is just the logic with argObject. I'm not concerned with the returned Result object or any other conditions, if only they could be proper enough to avoid any unrelated exceptions.

So I mocked a Result object, and it should be returned for any invocation of the sampleMethod (because the foo method expects a non-null Result object from the sampleMethod). And when the foo method is called, I should verify it that the argObject passed from the foo method is just as expected. Finally, I wrote the following code that reluctantly meets the test requirement.

def underlyingService
def testObject = new TestClass
def setup() {
    underlyingService = Mock(UnderlyingService)
    testObject.underlyingService = underlyingService
}
def "foo method should pass correct argObject"() {
    setup:
        def result = Mock(Result) {
            // mocked interation declaration
        }
    when:
        testObject.foo(input)
    then:
        1 * underlyingService.sampleMethod({it.bar == expectedBar}) >> result
    where:
        input | expectedBar
        xxx.  | xxxxxxxx
}

The test function will work well when the foo method is correctly implemented; but when not implemented correctly, it will report some unexpected failure information (the information I want it to show is that "argument is not as expected", but it will show NullPointerException about the result object in the foo method invocation; because the mocked sampleMethod is not matched, so a default Result object (null) will be returned, which is unexpected by the foo method).

Another important problem is that the sampleMethod in the then: clause (officially called "block", but maybe "clause" will be a better word, so I take it.) plays the role of Mocking and Verification at the same time. This is very confusing, because we will always think it that verifications and conclusions should be in the then: clause, but prerequisites should not. The prerequisites should be set in the setup: clause (of course, verifications should not be declared here in the setup: clause). So the ideal test code that will express my wish is like the following. But it always shows NullPointerException whether the foo method is implemented correctly or not. The reason of this is that the declaration in the then: clause will be matched when the method is invoked, not the declaration in the setup: clause; the declaration in the then: clause defined no Result, so the result returned to the foo method will be null.

def underlyingService
def testObject = new TestClass
def setup() {
    underlyingService = Mock(UnderlyingService)
    testObject.underlyingService = underlyingService
}
def "foo method should pass correct argObject"() {
    setup:
        def result = Mock(Result) {
            // mocked interation declaration
        }
        underlyingService.sampleMethod(_) >> result  // do mocking
    when:
        testObject.foo(input)
    then:
        1 * underlyingService.sampleMethod({it.bar == expectedBar}) // just do verification
    where:
        input | expectedBar
        xxx   | xxxxxxxx
}

That's all of my problem. I just wonder if there is any other way of writing the code that will satisfy my test requirement, or if we can have a discussion over the issue?

pniederw commented 9 years ago

There are good reasons why things work this way (see history of related discussions on the mailing list). The only mocking framework with different semantics that I'm aware of in the Java world is Mockito. If you prefer Mockito's semantics (which have their own tradeoffs), you can use Mockito together with Spock. To always return result, you'll need to add >> result to both interactions.

leonard84 commented 9 years ago

There is a third way, which I usually choose when I want to verify the arguments. Instead of using the arguments matcher to verify the argument, you can use the return closure since it also as access to the arguments. This way you get an assertion error if the argument did not match your expectations.

then:
  1 * underlyingService.sampleMethod(_) >> { args ->
         assert args[0].bar == expectedBar
         result
       }
xnslong commented 9 years ago

Thank you very much, @leonard84 @pniederw . The advice is really beneficial to me.

@pniederw Where can I check out the discussions on mail list? I'm interested in the reasons, but I didn't find where are the discussions. Is https://code.google.com/p/spock/issues/list the discussions history you mentioned?

leonard84 commented 9 years ago

Checkout the mailinglist https://groups.google.com/forum/#!forum/spockframework