Search code examples
javaspock

Java: Spock force to fail test on unrecorded mock invocation


I have java code and test written using groovy spock. Normally test follows this pattern

My sample Java Code

public User findUser(String id){
   return userRepo.findById(id);
}

my sample test

def "My Test"(){
  given:
  String id = "sample"

  and:
  1 * userRepoMock.findById(id) >> testUser

  when:
  User user = userServiceUnderTest.findUser(id);

  then:
  user == testUser
}

where and contains mock with invocation count.

Now imagine someone added another invocation to method in future. like

public User findUser(String id){
   anotherRepo.removeTypeById(id);
   return userRepo.findById(id);
}

even with this code change above test will pass without any modification. How can i tell spock to fail on unrecorded mock invocation. in this case anotherRepo.removeTypeById(id); . I want if someone add another invocation he/she forced to update the test correctly


Solution

  • IMO, you are overspecifying your test. This kind of test is bound to be brittle. You should not use interaction testing unless it is absolutely vital for verifying the correct application behaviour.

    Having said that, with what little information you provide in your question and some educated guesses, I think you might want something like

    0 * anotherRepoMock._(*_)
    

    Here is a full MCVE, which would have been your job to provide. Please do that next time, otherwise I am not going to answer again and others might not feel it is worth their time either.

    package de.scrum_master.stackoverflow.q70029352
    
    class User {
      String id
      String name
    
      boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false
    
        User user = (User) o
    
        if (id != user.id) return false
        if (name != user.name) return false
    
        return true
      }
    
      int hashCode() {
        int result
        result = id.hashCode()
        result = 31 * result + name.hashCode()
        return result
      }
    }
    
    package de.scrum_master.stackoverflow.q70029352
    
    class Another {
      String id
      int amount
    }
    
    package de.scrum_master.stackoverflow.q70029352
    
    class UserRepository {
      List<User> users = [
        new User(id: "a", name: "Alice"),
        new User(id: "b", name: "Bob"),
        new User(id: "c", name: "Claire")
      ]
    
      User findById(String id) {
        users.findAll { it.id == id }?.first()
      }
    }
    
    package de.scrum_master.stackoverflow.q70029352
    
    class AnotherRepository {
      List<Another> anothers = [
        new Another(id: "1", amount: 111),
        new Another(id: "2", amount: 222),
        new Another(id: "3", amount: 333)
      ]
    
      void removeTypeById(String id) {}
    }
    
    package de.scrum_master.stackoverflow.q70029352
    
    class UserService {
      UserRepository userRepo
      AnotherRepository anotherRepo
    
      User findUser(String id){
        anotherRepo.removeTypeById(id)
        userRepo.findById(id)
      }
    }
    
    package de.scrum_master.stackoverflow.q70029352
    
    import spock.lang.Specification
    
    class UserServiceTest extends Specification {
      UserRepository userRepoMock = Mock()
      AnotherRepository anotherRepoMock = Mock()
      UserService userServiceUnderTest = new UserService(userRepo: userRepoMock, anotherRepo: anotherRepoMock)
      User testUser = new User(id: "x", name: "Xander")
    
      def "My Test"() {
        given:
        String id = "sample"
    
        // Like Leonard said, it would be better to move these interactions
        // to the 'then:' block. I am just trying to make your code run
        // with minimal changes.
        and:
        1 * userRepoMock.findById(id) >> testUser
        0 * anotherRepoMock._(*_)
    
        when:
        User user = userServiceUnderTest.findUser(id);
    
        then:
        user == testUser
      }
    }
    

    Running this specification will yield the following error:

    Too many invocations for:
    
    0 * anotherRepoMock._(*_)   (1 invocation)
    
    Matching invocations (ordered by last occurrence):
    
    1 * anotherRepoMock.removeTypeById('sample')   <-- this triggered the error