Mockito不会模拟带有`when()`的方法调用。

huangapple go评论78阅读模式
英文:

Mockito won't mock method invokation with `when()`

问题

I'm building a REST Api using SpringBoot. I have three classes, `ProductController`, `BackendService`, and `SquareService`. All of these classes have a method called `postProduct(ProductRequestBody request)`, which returns different types based on the specific method. The flow of calls is as follows:

Here is the relevant part of the `ProductController`:

```java
@RestController
@Component
@Slf4j
public class ProductController {
    private final BackendService backendService;

    @PostMapping(value = "/products")
    public ResponseEntity<ResponseMessage> postProduct(@RequestBody ProductPostRequestBody request) {
        try {
            final BackendServiceResponseBody backendResponse = backendService.postProduct(request);
            final ProductResponseBody productResponse = ProductResponseBody.fromBackendResponseBody(backendResponse);
            return success("Successfully posted product!", productResponse);
        } catch (BackendServiceException exc) {
            return failure(exc, exc.getStatus());
        }
    }
}

The call to BackendService.postProduct() is here:
final BackendServiceResponseBody backendResponse = backendService.postProduct(request);

In a test class ControllerPostTests, Mockito is used to mock the call:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ControllerPostTests {
    @Autowired
    private ProductController controller;

    @MockBean
    private BackendService backendService;

    @Test
    public void testOnePost() {
        final ProductPostRequestBody request = ...;
        final BackendServiceResponseBody expected = ...;

        when(backendService.postProduct(request)).thenReturn(expected);

        final ResponseEntity<ResponseMessage> responseEntity = controller.postProduct(request);
        final ProductResponseBody response = checkAndGet(responseEntity);
        assertTrue("Request did not match response", responseMatchesPostRequest(request, response));
    }
}

The BackendService class contains the postProduct() method, and a call to SquareService.postProduct() is made inside it:

@Slf4j
@Component
public class BackendService {
    private final SquareService squareService;
    private final LiteProductRepository localRepo;

    public BackendServiceResponseBody postProduct(ProductPostRequestBody request) throws BackendServiceException {
        try {
            final SquareServiceResponseBody response = squareService.postProduct(request);
            localRepo.save(LiteProduct.fromSquareResponse(response));
            return BackendServiceResponseBody.fromSquareResponseBody(response);
        } catch (SquareServiceException exc) {
            throw new BackendServiceException(exc, exc.getStatus());
        }
    }
}

In a separate test class BackendPostTests, MockBean is used to mock SquareService:

@RunWith(SpringRunner.class)
@SpringBootTest
public class BackendPostTests {
    @Autowired
    private BackendService backendService;

    @MockBean
    private SquareService squareService;

    @MockBean
    private LiteProductRepository repository;

    @Test
    public void testOnePost() {
        final ProductPostRequestBody request = ...;
        final SquareServiceResponseBody expected = ...;

        when(squareService.postProduct(request)).thenReturn(expected);
        when(repository.save(any())).thenReturn(cachedMiniProduct);

        final BackendServiceResponseBody response = backendService.postProduct(request);
        assertTrue("Request did not match response", responseMatchesPostRequest(request, response));
    }
}

It seems that the call when(squareService.postProduct(request)).thenReturn(expected); in BackendPostTests is not working as expected.


<details>
<summary>英文:</summary>

I&#39;m building a REST Api using SpringBoot. I have three classes, `ProductController`, `BackendService` and `SquareService`. All of these classes have a method called `postProduct(ProductRequestBody request)`, which return different types based on which exact method we are talking about. So, `ProductController.postProduct(...)` calls `BackendService.postProduct()`, and `BackendService.postProduct()` calls `SquareService.postProduct()`, as the following flow graphic shows:

[![Anatomy of a POSt requestsin my API][1]][1]

 Here is what `Controller` looks like:

```java
@RestController // So no serving views of any kind
@Component
@Slf4j
public class ProductController
{
	private final BackendService backendService;
    .
    .
    .
	@PostMapping(value = &quot;/products&quot;)
	public ResponseEntity&lt;ResponseMessage&gt; postProduct(@RequestBody ProductPostRequestBody request)
	{
		if(!LiteProduct.PRODUCT_TYPES.contains(request.getProductType().toUpperCase()))
		{
			return failure(&quot;Invalid product type provided: Valid categories are: &quot; +
			               new ArrayList&lt;&gt;(LiteProduct.PRODUCT_TYPES) + &quot;.&quot;,
			               HttpStatus.BAD_REQUEST);
		}
		else if(request.getCostInCents() &lt; 0)
		{
			return failure(&quot;Negative cost provided: &quot; + request.getCostInCents() +&quot;.&quot;,
			               HttpStatus.BAD_REQUEST);
		}
		else
		{
			try
			{

               /* ************************************************************** */
               /* ****** Here is the call to BackendService.postProduct() ****** */
               /* ************************************************************** */

				final BackendServiceResponseBody backendResponse = backendService.postProduct(request);

				final ProductResponseBody productResponse = ProductResponseBody.fromBackendResponseBody(backendResponse);
				return success(&quot;Successfully posted product!&quot;, productResponse);
			}
			catch (BackendServiceException exc)
			{
				return failure(exc, exc.getStatus());
			}
		}
	}
}

You can see the call to BackendService.postProduct() above:
final BackendServiceResponseBody backendResponse = backendService.postProduct(request);

I have used Mockito successfully to mock that call through a class called ControllerPostTests:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ControllerPostTests
{

	@Autowired
	private ProductController controller; // The class we are testing

	@MockBean
	private BackendService backendService;     // The class that will be mocked
    .
    .
    .

    @Test
	public void testOnePost()
	{
		final ProductPostRequestBody request = ProductPostRequestBody
													.builder()
														.name(&quot;Culeothesis Necrosis&quot;)
														.productType(&quot;Flower&quot;)
														.costInCents(600L) // &#39;L for long literal
													.build();

		final BackendServiceResponseBody expected = BackendServiceResponseBody.builder()
			                                                                .name(request.getName())
			                                                                .itemId(&quot;RANDOM_ITEM_ID&quot;)
			                                                                .itemVariationId(&quot;RANDOM_ITEM_VAR_ID&quot;)
			                                                                .productType(request.getProductType())
			                                                                .costInCents(request.getCostInCents())
			                                                                .isDeleted(false)
		                                                                .build();

       /* ***************************************************************************** */
       /* ************ Here&#39;s the call that gets successfully mocked: ***************** */
       /* ***************************************************************************** */

		when(backendService.postProduct(request)).thenReturn(expected);

		final ResponseEntity&lt;ResponseMessage&gt; responseEntity = controller.postProduct(request);
		final ProductResponseBody response = checkAndGet(responseEntity);
		assertTrue(&quot;Request did not match response&quot;, responseMatchesPostRequest(request, response));
	}

And this test, alongside a more contrived one, works perfectly, and also passes (yay!).

Now, what about BackendService? I want to mock the SquareService to debug that too, and I will also have to mock a class called LiteRepository which is effectively a JPARepository:

@Slf4j
@Component
public class BackendService
{
	private final SquareService squareService; // Another class that is called by the methods of `this`.
	private final LiteProductRepository localRepo;  // A JPARepository I&#39;m using as a cache.
    .
    .
    .
	public BackendServiceResponseBody postProduct(ProductPostRequestBody request) throws BackendServiceException
	{
		// First, make a local check to ensure that there&#39;s no name clash for
		// the product uploaded. This is one of the advantages of having a cache.
		if(localRepo.findByName(request.getName()).isPresent())
		{
			final ResourceAlreadyCreatedException exc = new ResourceAlreadyCreatedException();
            logException(exc, this.getClass().getEnclosingMethod().getName());
            throw new BackendServiceException(exc, HttpStatus.CONFLICT);
		}
		else
			// We first POST to Square and *then* store the cached version in our
			// local DB in order to grab the unique ID that Square provides us with.
		{
			try
			{
               /* ************************************************************************* */
               /* ********** Here&#39;s the call that I need mocked: *************************** */
               /* ************************************************************************* */

				final SquareServiceResponseBody response = squareService.postProduct(request);

               /* ************************************************************************* */
               /* **** This call also needs to be mocked, but let&#39;s deal with it later.**** */
               /* ************************************************************************* */

				localRepo.save(LiteProduct.fromSquareResponse(response));
				return BackendServiceResponseBody.fromSquareResponseBody(response);
			}
			catch(SquareServiceException exc)
			{
				logException(exc, this.getClass().getEnclosingMethod().getName());
                throw new BackendServiceException(exc, exc.getStatus());
			}
		}
	}

What baffles me is that, in a separate test file, I have followed exactly the same approach that I have followed in my ProductControllerTests, but unfortunately the call is not mocked, and the code actually executes the method SquareService.postProduct(...), which I need mocked:

@RunWith(SpringRunner.class)
@SpringBootTest
public class BackendPostTests
{

	/* *********************************************************************************************************** */
	/* ************************************ Fields and utilities ************************************************** */
	/* *********************************************************************************************************** */

	@Autowired
	private BackendService backendService; // The class we are testing

	@MockBean
	private SquareService squareService;     // One class that will be mocked

	@MockBean
	private LiteProductRepository repository;     // Another class that will be mocked

     .
     .
     .

	@Test
	public void testOnePost()
	{
		final ProductPostRequestBody request = ProductPostRequestBody
													.builder()
														.name(&quot;Pink handbag&quot;)
														.productType(&quot;Accessory&quot;)
														.costInCents(600L) // &#39;L for long literal
													.build();

		final SquareServiceResponseBody expected = SquareServiceResponseBody.builder()
						                                                  .name(request.getName())
						                                                  .itemId(&quot;RANDOM_ITEM_ID&quot;)
						                                                  .itemVariationId(&quot;RANDOM_ITEM_VAR_ID&quot;)
						                                                  .productType(request.getProductType())
						                                                  .costInCents(request.getCostInCents())
						                                                  .isDeleted(false)
		                                                             .build();
                 
       /* *********************************************************************** */
       /* ******* This is where I mock SquareService.postProduct() ************** */
       /* *********************************************************************** */
		when(squareService.postProduct(request)).thenReturn(expected);

		final LiteProduct cachedMiniProduct = LiteProduct.fromSquareResponse(expected);

       /* *********************************************************************** */
       /* * And this is where I hope to mock LiteProductRepository.save() later * */
       /* *********************************************************************** */
		when(repository.save(cachedMiniProduct)).thenReturn(cachedMiniProduct);
		
		final BackendServiceResponseBody response = backendService.postProduct(request);
		assertTrue(&quot;Request did not match response&quot;, responseMatchesPostRequest(request, response));
	}

! I have been able to determine that the code definitely goes inside that method through the IntelliJ debugger. The line when(squareService.postProduct(request)).thenReturn(expected);does not seem to work.

What am I doing wrong here?

// Edit: Improved image.

答案1

得分: 1

你应该尝试使用Mockito.any(ProductPostRequestBody.class),这样看起来会像这样。

when(squareService.postProduct(Mockito.any(ProductPostRequestBody.class))).thenReturn(expected);
英文:

You should try Mockito.any(ProductPostRequestBody.class) so this would look like.

when(squareService.postProduct(Mockito.any(ProductPostRequestBody.class))).thenReturn(expected);

huangapple
  • 本文由 发表于 2020年10月23日 22:45:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/64502203.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定