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);
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();