Search code examples
javagroovymockingmockitospock

Spock groovy - how to mock methods in same class?


How to mock the private method and method in a different class in the test class?

class MyClass {
    private final Retriever<ScoreData> retriever;
    private DataStore<Model> dataStore;
    private String gameName;

    public void MyClass(Retriever<ScoreData> retriever, DataStore<Model> dataStore, String gameName) {
        this.retriever = retriever;
        this.dataStore = dataStore;
        this.gameName = gameName;
    }

    public void process(GameHolder<G> games) {
        // Business Logic
        for (Game<G> game : games){
        Integer score = game.getScore();
        Integer playerId = game.getPlayerId();
        Integer finalScore = getScore(game);
        computeScore(score, finalScore);
        }
    }

    private Integer computeScore(int score, int finalScore) {
        // Runs some business logic and returns O3
        return score + finalScore;
    }

    private Integer getScore(Game game) {
        // Runs some business logic and returns O3
        String dbName = game.getDbName();
        DBRetriever ret = new DBRetriever(dbName)
        if (dbName.equals("gameDB"){
            return ret.getFinalScore(dbName);
        }
        return -1;
    }

}

Below is my current implementation for Spock and I am not sure how to implement the mocking for objects.

@Subject
def obj

def "this is my test"(){
    given:
    Object1 obj1 = Mock(Object1)
    Object2 obj2 = Mock(Object2)
    Object3 obj3 = Mock(Object3)

    def myClassObject = new MyClass(obj1, obj2, obj3)

    when:
    myClassObject.process(new Object4())

    then:
    1 * getScore()
    1 * computeScore()

}

How can I mock the computeScore and getScore functions and how can I assign the initial values for objects obj1, obj2, obj3?

Note: I am only trying to test process() method here. But process method is calling a private method from inside. I want to be able to return a mock value for private method rather than executing the private method.

Edit: Retriever and DataStore are interfaces and their respective implementations are ScoreData and Model.


Solution

  • Note: I am only trying to test process() method here. But process method is calling a private method from inside. I want to be able to return a mock value for private method rather than executing the private method.

    You should not do that because MyClass is your class under test. You cannot cover the logic inside the private methods with tests if you stub them. Instead, you should make sure that the injected mocks behave the way you want them to (via stubbed methods) if they are used inside those private method. Unfortunately you decided not to show that crucial part of your code even though the exact answer depends on it. Instead you replaced them with comments "some business logic", which is not very helpful because your business logic is what you want to test. You don't want to stub it out.


    So please don't do what I am showing you here, I am answering only because you asked.

    In order to stub a method it must not be private because spies, mocks or stubs technically are always subclasses or the originals and subclasses cannot inherit or even call private methods. Thus, you need to make the methods protected (so subclasses can use or override them) or package-scoped. I recommend the former.

    But you cannot use a normal mock or stub as a stand-in for your class under test because you only want to stub out part of the business logic (your two methods in question), not the whole logic (you want to keep process()). Thus, you need a partial mock. For this purpose you can use a spy.

    Dummy dependency classes:

    package de.scrum_master.stackoverflow.q60103582;
    
    public class Object1 {}
    
    package de.scrum_master.stackoverflow.q60103582;
    
    public class Object2 {}
    
    package de.scrum_master.stackoverflow.q60103582;
    
    public class Object3 {}
    
    package de.scrum_master.stackoverflow.q60103582;
    
    public class Object4 {}
    

    Class under test:

    package de.scrum_master.stackoverflow.q60103582;
    
    public class MyClass {
      private Object1 o1;
      private Object2 o2;
      private Object3 o3;
    
      public MyClass(Object1 o1, Object2 o2, Object3 o3) {
        this.o1 = o1;
        this.o2 = o2;
        this.o3 = o3;
      }
    
      public void process(Object4 o4) {
        System.out.println("process - business Logic");
        Object2 result = getScore("dummy ID");
        Object3 obj = computeScore(result);
      }
    
      protected Object3 computeScore(Object2 result) {
        System.out.println("computeScore - business logic");
        return o3;
      }
    
      protected Object2 getScore(String id) {
        System.out.println("getScore - business logic");
        return o2;
      }
    }
    

    Spock test:

    package de.scrum_master.stackoverflow.q60103582
    
    import spock.lang.Specification
    
    class MyClassTest extends Specification {
      def "check main business logic"(){
        given:
        Object1 obj1 = Mock()
        Object2 obj2 = Mock()
        Object3 obj3 = Mock()
    
        MyClass myClass = Spy(constructorArgs: [obj1, obj2, obj3])
    
        when:
        myClass.process(new Object4())
    
        then:
        1 * myClass.getScore(_) //>> obj2
        1 * myClass.computeScore(_) //>> obj3
      }
    }
    

    Here you can see how to check interactions on the spy. But note that computeScore(_) and getScore(_) will still be executed, as you can see in the console log:

    process - business Logic
    getScore - business logic
    computeScore - business logic
    

    If you uncomment the end of the last two lines of code

        1 * myClass.getScore(_) >> obj2
        1 * myClass.computeScore(_) >> obj3
    

    you will actually avoid the two (protected) methods from being executed altogether and replace them by stub results. The console log will change to:

    process - business Logic
    

    But I am saying it again: Don't do this. Instead make sure your injected mocks show the right behaviour so you can actually execute the methods in your class under test. This is what testing is about, isn't it?