Search code examples
springspring-bootmockitojunit4

spring mockito test failed when call a mock method


here are two Spring Service;

public interface Service01 {
     String test01(String name);
}

@Service 
public class Service01Impl implements Service01 { 

     @Override
     public String test01(String name) {
          if (!"123".equals(name)) {
              throw new RuntimeException("name is illegal");
          }
          return name;
     }
}

@Service 
public class Service02 { 
     @Autowired
     private Service01 service01;
     @Autowired
     private Service03 service03;

     public String test02(String name) {
          service03.checkNameNotNull(name);
          return service01.test01(name);
     }
}

@Service 
public class Service03 { 
     public void checkNameNotNull(String name) {
          if (name == null) {
              throw new RuntimeException("name is null");
          }
     }
}

then here is my testClass:

public class TestCalss {
    @Mock
    private Service01 service01;
    @Autowired
    private Service02 service02;

    @Test
    public void testMock() {
        // mock service
        when(service01.name("abc")).thenReturn("mock return abc");

        // here will be success
        String nameAbc = service01.test01("abc")
        Assert.assertEquals("mock return abc", nameAbc);

        // but here got exception: `name is illegal`
        String name = service02.test02("abc");
        Assert.assertEquals("mock return abc", name);
    }

}

Q1: why got the exception at row: String name = service02.test02("abc");

java.lang.RuntimeException: name is illegal

Q2: how to mock Service01 when I call Service02?

-- edit

Q2 fixed with @InjectMocks

Q3: how to only mock Service01, but Service03 no mock.

I want to make this check have no mock service03.checkNameNotNull(name);


Solution

  • You need to use an ArgumentMatcher when you set up your mock - instead of passing in the String literal "abc", you need your line to look like when(service01.name(eq("abc"))).thenReturn...

    If you don't mind what the actual string that gets passed in is, then there are other matchers you can use like anyString() as well.

    You also don't want to use @Autowired in your test - if you want the class under test to have mocks injected into it automatically then you need to instantiate the mocks and have them injected rather than the real Spring beans.

    The most straightforward way to do this would be to use the MockitoJUnitRunner and the @InjectMocks annotation:

    @RunWith(MockitoJUnitRunner.class)
    public class TestCalss {
        @Mock
        private Service01 service01;
        @InjectMocks
        private Service02 service02;
    
    ...
    

    To inject a class that isn't a mock, you need to use a @Before annotation, with some way of passing the object into the class (like you would in plain Java code).

    My preferred way to do this is to have a constructor that sets the dependencies as private final fields on the class, but Spring also provides a class ReflectionTestUtils that can be used if you really want to stick with private fields with no setters.

    So something like this:

    @Service 
    public class Service02 { 
    
         private final Service01 service01;
         private final Service03 service03;
    
         @Autowired
         public Service02 (Service01 service01, Service03 service03) {
             this.service01 = service01;
             this.service03 = service03;
         }
    
        ...
    }
    
    @RunWith(MockitoJUnitRunner.class)
    public class TestClass {
    
        @Mock
        private Service01 service01;
    
        private Service02 underTest;
    
        @Before
        public void setup() {
            this.underTest = new Service02(this.service01, new Service03());
        }
        ...
    }
    

    If you want to call a real method on an object that you otherwise want to mock, then that may be a sign that your class is doing too much and should actually be two or more smaller classes, but it's also possible: when(someMock.someMethod()).thenCallRealMethod();