javaunit-testinggroovyspockspy

how to use spock to skip some inner void methods


code:

public class A{
    public void method(User user){
        String name = user.getName();
        if("Tom".equals(name)){
            method1(user);
        }else{
            method2(user);
        }
    }
}

I want to write a test about A#method() by using spock. As you can see,When I test method(),I don't care the execution of method1() or method2(),I just want to verify that those two methods will invoked in certain case. My test code:

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
class ASpec extends Specification {
   
    def "method"(){
        given:
        def process = new A()

        expect:
        process.method(input as User)

        where:
        input                || _
        new User(name:"Tom") || _
    }
}

I find that method1() does executed,but I think that the meaning of a unit test is just to verify the process is right, so I don't want method1() executed actually,but just invoked. What should I do?

p.s. I also use spy:

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
class ASpec extends Specification {

    def "method"(){
        given:
        A process = Spy()

        expect:
        process.method(input as User)

        where:
        input                || _
        new User(name:"Tom") || _
    }
}

but the method1() also executed.


Solution

  • Leonard is totally right: In >99% of normal unit tests

    What you want is technically possible, but it usually is a code smell, if it is necessary. For argument's sake, let us assume your class under test looks like this:

    class A {
      void method(User user) {
        if ('Tom' == user.name)
          method1(user)
        else
          method2(user)
      }
    
      void method1(User user) {
        println "method1, doing something expensive & slow with $user.name"
      }
    
      void method2(User user) {
        println "method2, doing something expensive & slow with $user.name"
      }
    }
    
    class User {
      String name
    }
    

    Now you can test like this:

    import spock.lang.Specification
    import spock.lang.Unroll
    
    class ATest extends Specification {
      @Unroll('user #user.name')
      def 'expensive calculation (no mocks)'() {
        given:
        def process = new A()
    
        expect:
        process.method(user)
    
        where:
        user << [new User(name: 'Tom'), new User(name: 'Tina')]
      }
    
      @Unroll('user #user.name')
      def 'expensive calculation (spy)'() {
        given:
        A process = Spy()
    
        when:
        process.method(user)
    
        then:
        1 * process."$calledMethod"(user) >> null
    
        where:
        user                   | calledMethod
        new User(name: 'Tom')  | 'method1'
        new User(name: 'Tina') | 'method2'
      }
    

    While in the first test, you will see the log output for the called methods, for the second one you will not, because the two methods have been stubbed out. By default, a spy calls the original method, if you do not change its behaviour.

    Leonard also said that you should refactor, if the helper methods do something that is not the core responsibility of the class under test. Let us assume, we have this case here. Then you can refactor and test like this:

    class B {
      ExpensiveCalculation calculation
    
      void method(User user) {
        if ('Tom' == user.name)
          this.calculation.method1(user)
        else
          this.calculation.method2(user)
      }
    }
    
    class ExpensiveCalculation {
      void method1(User user) {
        println "method1, doing something expensive & slow with $user.name"
      }
    
      void method2(User user) {
        println "method2, doing something expensive & slow with $user.name"
      }
    }
    
      @Unroll('user #user.name')
      def 'expensive calculation (factored out, mocked)'() {
        given:
        def calculation = Mock(ExpensiveCalculation)
        def process = new B(calculation: calculation)
    
        when:
        process.method(user)
    
        then:
        1 * calculation."$calledMethod"(user)
    
        where:
        user                   | calledMethod
        new User(name: 'Tom')  | 'method1'
        new User(name: 'Tina') | 'method2'
      }
    

    See how easy it is to inject a mock and verify its behaviour? No more ugly self-spying on the class under test, but clear separation of concerns. But before you use interaction testing (verifying behaviour), think about whether you need it at all. Just because it is possible, it does not mean it is always necessary.

    Try it in the Groovy Web Console and click on the "Result" tab to see the test report.