Ravish Khatri
Ravish Khatri

Reputation: 23

Spock stubbing not working as expected. Stubbed method returning null instead of mock response

I am trying out spock framework for writing unit test for a particular method in a service class but not able to mock response for a method.

Service class

@Service
@RequiredArgsConstructor
public class ServiceA  {
    private final RepositoryA repositoryA;
    private final ServiceB serviceB;

    public void methodUnderTest(String arg1,String arg2){
         ObjectA objectA = repositoryA.someMethod(arg1, arg2); // here objectA is null

        if (objectA != null) {
           // Do something
        }else{
          // Do something else
        }
    }
}

I am not able to stub repositoryA.someMethod(arg1, arg2) for some reason. Its returning null while running the test. I have tried putting both mock and stub interaction in then block as suggested in another question but its not working for me.

Test class

class ServiceATest extends Specification {
    @Shared ServiceA serviceA;

    @Shared RepositoryA repositoryA =  Mock(RepositoryA)
    @Shared ServiceB serviceB = Mock(ServiceB)


    def "test"() {
        given:
        serviceA = new ServiceA(repositoryA, serviceB);
        
        ObjectA mockResponse = ObjectA.builder()
                .field1("demo")
                .field2("demo")
                .field3(Instant.now().plus(3, ChronoUnit.DAYS))
                .build();

        when:
        serviceA.methodUnderTest(arg1,arg2);

        then:
        1 * repositoryA.someMethod(arg1,arg2) >> mockResponse // this is not working as expected
    }
}

Upvotes: 0

Views: 41

Answers (1)

kriegaex
kriegaex

Reputation: 67287

Spock manual, chapter Scope of Interactions:

Interactions are always scoped to a particular feature method. Hence they cannot be declared in a static method, setupSpec method, or cleanupSpec method. Likewise, mock objects should not be stored in static or @Shared fields.

Why would you even want to share a mock? Sharing state is bad test design and should only be used as sparingly as possible for expensive objects. Mocks are cheap. Simply resist the reflex to make everything @Shared, especially mocks. This kind of premature optimisation is equivalent to shooting your own foot.

For reference, here is an MCVE reproducing your situation. It would have been your job to provide one:

package de.scrum_master.stackoverflow.q77973193

import spock.lang.Specification

import java.time.Instant
import java.time.temporal.ChronoUnit

class ServiceATest extends Specification {
  ServiceA serviceA
  RepositoryA repositoryA = Mock(RepositoryA)
  ServiceB serviceB = Mock(ServiceB)

  def "test"() {
    given:
    serviceA = new ServiceA(repositoryA, serviceB)
    String arg1 = "one"
    String arg2 = "two"

    ObjectA mockResponse = ObjectA.builder()
      .field1("demo")
      .field2("demo")
      .field3(Instant.now().plus(3, ChronoUnit.DAYS))
      .build()

    when:
    serviceA.methodUnderTest(arg1, arg2)

    then:
    1 * repositoryA.someMethod(arg1, arg2) >> mockResponse
  }
}
package de.scrum_master.stackoverflow.q77973193

class ServiceB {}
package de.scrum_master.stackoverflow.q77973193

class ServiceA {
  RepositoryA repositoryA
  ServiceB serviceB

  ServiceA(RepositoryA repositoryA, ServiceB serviceB) {
    this.repositoryA = repositoryA
    this.serviceB = serviceB
  }

  void methodUnderTest(String s1, String s2) {
    repositoryA.someMethod(s1, s2)
  }
}
package de.scrum_master.stackoverflow.q77973193

import java.time.Instant

class RepositoryA {
  ObjectA someMethod(String s1, String s2) {
    ObjectA.builder()
      .field1(s1)
      .field2(s2)
      .field3(Instant.now())
      .build()
  }
}
package de.scrum_master.stackoverflow.q77973193

import java.time.Instant

class ObjectA {
  String field1
  String field2
  Instant field3

  static Builder builder() {
    new Builder()
  }

  static class Builder {
    ObjectA instance = new ObjectA()

    Builder field1(String value) {
      instance.field1 = value
      this
    }

    Builder field2(String value) {
      instance.field2 = value
      this
    }

    Builder field3(Instant value) {
      instance.field3 = value
      this
    }

    ObjectA build() {
      instance
    }
  }
}

Try it in the Groovy Web Console.

Upvotes: 0

Related Questions