英文:
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'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 = "/products")
public ResponseEntity<ResponseMessage> postProduct(@RequestBody ProductPostRequestBody request)
{
if(!LiteProduct.PRODUCT_TYPES.contains(request.getProductType().toUpperCase()))
{
return failure("Invalid product type provided: Valid categories are: " +
new ArrayList<>(LiteProduct.PRODUCT_TYPES) + ".",
HttpStatus.BAD_REQUEST);
}
else if(request.getCostInCents() < 0)
{
return failure("Negative cost provided: " + request.getCostInCents() +".",
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("Successfully posted product!", 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("Culeothesis Necrosis")
.productType("Flower")
.costInCents(600L) // 'L for long literal
.build();
final BackendServiceResponseBody expected = BackendServiceResponseBody.builder()
.name(request.getName())
.itemId("RANDOM_ITEM_ID")
.itemVariationId("RANDOM_ITEM_VAR_ID")
.productType(request.getProductType())
.costInCents(request.getCostInCents())
.isDeleted(false)
.build();
/* ***************************************************************************** */
/* ************ Here's the call that gets successfully mocked: ***************** */
/* ***************************************************************************** */
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));
}
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'm using as a cache.
.
.
.
public BackendServiceResponseBody postProduct(ProductPostRequestBody request) throws BackendServiceException
{
// First, make a local check to ensure that there'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's the call that I need mocked: *************************** */
/* ************************************************************************* */
final SquareServiceResponseBody response = squareService.postProduct(request);
/* ************************************************************************* */
/* **** This call also needs to be mocked, but let'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("Pink handbag")
.productType("Accessory")
.costInCents(600L) // 'L for long literal
.build();
final SquareServiceResponseBody expected = SquareServiceResponseBody.builder()
.name(request.getName())
.itemId("RANDOM_ITEM_ID")
.itemVariationId("RANDOM_ITEM_VAR_ID")
.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("Request did not match response", 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);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论