Search code examples
javaunit-testingmockingspock

"Too few invocations" but method is invoked and everything is created using GroovySpy or GroovyMock


I have class like this:

class A(B b, C c, D d, E e) {

   protected void doSth() {
       test();
   }

   protected void test() {
      System.out.println("test");
   }
}

and I am trying to test if doSth() method is invoked then test() method is also invoked. Methods are of returned type void so I cannot test result of these methods.

My tests looks like this

    def "test when doSth"() {
        given: 'A class'
        A aClass = GroovySpy(A, constructorArgs: [GroovyMock(B), GroovyMock(C), GroovyMock(D), GroovyMock(E)) as A

        when:
        A.doSth()

        then:
        1 * A.test()
    }

but this test does not work. It gives me "Too few invocations" error for test method. I am really confused because exactly same example but with no args in constructor works perfectly.


Solution

  • Firstly, I don't know what kind of syntax your Java class has. I guess it is not really Java but maybe something like Kotlin? I have no idea, I only speak Java and Groovy when it comes to JVM languages. So let me recreate your subject under test like this:

    package de.scrum_master.stackoverflow.q56652868;
    
    public class A {
      private B b;
      private C c;
      private D d;
      private E e;
    
      public A(B b, C c, D d, E e) {
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
      }
    
      protected void doSth() {
        test();
      }
    
      protected void test() {
        System.out.println("test");
      }
    
      public static class B {}
      public static class C {}
      public static class D {}
      public static class E {}
    }
    

    I hope this is okay.

    As for your test, I am not sure if testing this interaction (method test() called by doSth()) is really essential to your application and should be tested in the first place. I think this kind of interaction testing is important for certain design patterns where you e.g. want to check if e.g. a subject notifies certain registered observers when something specific happens. Testing the inner wiring of a class, especially non-public methods, usually does not do much good and can lead to over-specification and brittle tests which need to be updated often if not the class's public API but just the internal implementation changes.

    Having said that, now let me answer your question anyway for educational purposes.


    Several things are wrong in your test:

    • The syntax is wrong. You are missing a closing ] for the constructor arguments list in the line defining your Groovy spy.
    • Not really a mistake, but why over-specify the type of your spy? Instead of A aClass = GroovySpy(A, ...) as A you make 3x sure it is really an A. Why not just A aClass = GroovySpy(...) or maybe def aClass = GroovySpy(A, ...)? No need to also convert an A into itself via as A.
    • Don't use Groovy mocks and spies, normal Spock mocks and spies will do. You don't need the advanced Groovy mock features and furthermore for target classes which are not themselves implemented in Groovy those features would not work anyway, as is documented in the Spock manual. They would just behave like normal Spock mocks.
    • Last, but not least there is the real error: You write A.doSth() and A.test() as if you were dealing with static methods. Why? You need to write aClass.doSth() and aClass.test(), then your test will pass:
    package de.scrum_master.stackoverflow.q56652868
    
    import de.scrum_master.stackoverflow.q56652868.A.B
    import de.scrum_master.stackoverflow.q56652868.A.C
    import de.scrum_master.stackoverflow.q56652868.A.D
    import de.scrum_master.stackoverflow.q56652868.A.E
    import spock.lang.Specification
    
    class ATest extends Specification {
      def "test when doSth"() {
        given: 'A class'
        A aClass = Spy(constructorArgs: [Mock(B), Mock(C), Mock(D), Mock(E)])
    
        when:
        aClass.doSth()
    
        then:
        1 * aClass.test()
      }
    }