Gustavo Cruz
Gustavo Cruz

Reputation: 68

How to use @SpyBean in a class that receives arguments by the constructor?

I'm creating some unit tests for a Spring Boot application.

In a class called ComicService there is a method called getComicByApi and I want to create a test for that method, but this method accesses another method of the same class called getHash.

I need to configure the behavior of getHash, so I used the @SpyBean annotation in creating the ComicService object.

The problem is that when running the test it gives an error in the part where I use Mockito.when().thenReturn() to configure the behavior of getHash.

I found that the error is related to the fact that I use @BeforeEach public void setUp() to instantiate the annotated class with @SpyBean passing its constructor arguments, but I still don't know how to solve it.

Does anyone know how to solve this problem?

ComicService

@Service
public class ComicService {
    
    private String publicKey;
    
    private String privateKey;
    
    private MarvelClient marvelClient;
    
    public ComicService(@Value("${marvel.public_key}")String publicKey, 
            @Value("${marvel.private_key}") String privateKey, MarvelClient marvelClient) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
        this.marvelClient = marvelClient;
    }
    
    public MarvelAPIModelDTO getComicByApi(Integer idComicMarvel) {
        String timeStamp = String.valueOf((int)(System.currentTimeMillis() / 1000));
        String hash = getHash(timeStamp);
        
        MarvelAPIModelDTO comic = marvelClient.getComic(idComicMarvel, timeStamp, timeStamp, hash);
        
        return comic;
    }
    
    public String getHash(String timeStemp) {
        String value = timeStemp+privateKey+publicKey;          
        
        MessageDigest md;
        
        try {
            md = MessageDigest.getInstance("MD5");
        } catch(NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        
        BigInteger hash = new BigInteger(1, md.digest(value.getBytes()));
    
        return hash.toString(16);
    }

}

ComicServiceTest

@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
public class ComicServiceTest {
    
    @SpyBean
    ComicService comicService;
    
    @MockBean
    MarvelClient marvelClient;
    
    @BeforeEach
    public void setUp() {
        this.comicService = new ComicService("ae78641e8976ffdf3fd4b71254a3b9bf", "eb9fd0d8r8745cd0d554fb2c0e7896dab3bb745", marvelClient);      
    }

    @Test
    public void getComicByApiTest() {
        // Scenario
        MarvelAPIModelDTO foundMarvelAPIModelDTO = createMarvelAPIModelDTO();
        
       //It's giving an error on this line 
        Mockito.when(comicService.getHash(Mockito.anyString())).thenReturn("c6fc42667498ea8081a22f4570b42d03"); 

        Mockito.when(marvelClient.getComic(Mockito.anyInt(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(foundMarvelAPIModelDTO);
        
        // Execution
        MarvelAPIModelDTO marvelAPIModelDTO = comicService.getComicByApi(1);
        
        // Verification
        Assertions.assertThat(marvelAPIModelDTO.getData().getResults().get(0).getId()).isEqualTo(1);        
    }

}

Error

at com.gustavo.comicreviewapi.services.ComicServiceTest.getComicByApiTest(ComicServiceTest.java:58)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(any());
    verify(mock).someMethod(contains("foo"))

This message may appear after an NullPointerException if the last matcher is returning an object 
like any() but the stubbed method signature expect a primitive argument, in this case,
use primitive alternatives.
    when(mock.get(any())); // bad use, will raise NPE
    when(mock.get(anyInt())); // correct usage use

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
Mocking methods declared on non-public parent classes is not supported.

Upvotes: 2

Views: 4826

Answers (2)

Evgeniy Zhurenko
Evgeniy Zhurenko

Reputation: 45

You can use ,@MockBean annotation instead @SpyBean and can use Mockito.when().thenCallRealMethod() as you need to call real method and Mockito.when().thenReturn() at the same time if you want to mock some method. This way have to rid you of all your problems in that case and you can mock as you wanted.

For example :

@Component
class MyClass {

public String firstMethod(String value){
    return value;
}

public void secondMethod(String value){
    some code
}

@Component
class Main {
    @Autowire
    private MyClass clazz;
    
    public String getFirstMethod(String value){
        some code;
    }

    public void getSecondMethod(String value){
        clazz.secondMethod(value);
    }
}

In test class you have to declare a mock bean of your class as below and to mock them as you want. Instead thenReturn() you can use thenAnswer(), thenThrows(), then(), and instead doNothing() for void methods you can use doAnswer(), doThrow(), doReturn():

@MockBean(classes={MyClass.class})
public MainTest {

@Autowire
private Main main
@Autowire
privat MyClass clazz;

    @Test
    void getFirstMethod_mock(){
        String value = "something";
        when(clazz.firstMethod(anyString()).thenReturn("some string value");
        
        String actual = main.getFirstMethod(value );

        assertEquals("some string value", actual);
    
    }

    @Test
    void getFirstMethod_realMethod(){
        String value = "something";
        when(clazz.firstMethod(anyString()).thenCallRealMethod();
        
        String actual = main.getFirstMethod(value );

        assertEquals(realResultOfFirstMethod, actual);
    
    }

    @Test
    void getSecondMethod_mock(){
        String value = "something";
        doNothing().when(clazz).secondMethod(anyString());
        
        main.etSecondMethod(value );
    
    }

    @Test
    void getSecondMethod_realMethod(){
        String value = "something";
        doCallRealMethod().when(clazz).secondMethod(anyString());
        
        main.etSecondMethod(value );
    
    }

}

If your class not Bean you can declar it as below :

MyClass clazz = mock(MyClass.class);

The methods still stay the same : when(clazz.method()).then() and for void doNothing().when(clazz).method().

Upvotes: 2

Ashish Patil
Ashish Patil

Reputation: 4604

It looks like @SpyBean is unable to find instance of your service class. Quick alternative for this is just remove @SpyBean from ComicService comicService; & do following in your @BeforeEach:

@BeforeEach
    public void setUp() {
        this.comicService = Mockito.spy(new ComicService("ae78641e8976ffdf3fd4b71254a3b9bf", "eb9fd0d8r8745cd0d554fb2c0e7896dab3bb745", marvelClient));

    }

Here, you are creating spy & then using it inside your test class.

Upvotes: 3

Related Questions