【问题标题】:Unit test does not work as expected - instead of 404, it returns 200 as status code单元测试没有按预期工作 - 而不是 404,它返回 200 作为状态码
【发布时间】:2020-05-11 09:02:32
【问题描述】:

我正在为一个应用程序编写单元测试,该应用程序将具有特定货币的金额转换为欧元。我在数据库中有一些帐户:iban、货币和余额。

为了获取所有货币及其以欧元为单位的价值,需要调用 API 以获取汇率。转换方法接收账户的 iban 并从账户中返回欧元的金额。 该应用程序运行良好,但单元测试存在一些问题。

我正在尝试为找不到帐户的情况编写单元测试 - 所以当 AccountController 类中的方法 - getAccountByIban 返回 404 时。我编写了测试并添加了一个随机字符串作为 IBAN - 但是而不是 404,我得到 200 作为响应代码并且测试失败。如果我使用测试中的随机 IBAN 调用方法 getAccountByIban,我会得到正确的响应代码 - 404。

这是我的代码:

帐户控制器:

@RestController
public class AccountController {

    @Autowired
    RequestBuilder requestBuilder;

    @Autowired
    AccountService accountService;

    @ApiOperation(value = "Get account with given iban ", response = AccountEntity.class)
    @RequestMapping(value = "/account/{iban}", method = RequestMethod.GET )
    public AccountDTO getAccountByIban(@PathVariable("iban") String iban ) throws AccountNotFoundException {

        return Optional
                .ofNullable(accountService.getAccountByIban(iban))
                .orElseThrow(() -> new AccountNotFoundException("Requested account was not found! [ iban = " + iban + "]"));
    }

    @ApiOperation(value = "Get account with amount converted ", response = AccountEntity.class)
    @RequestMapping(value = "/account/exchange/{iban}", method = RequestMethod.GET )
    public AccountDTO getAmountConvertedByIban(@PathVariable("iban") String iban) {

        AccountDTO accountDTO = accountService.getAccountByIban(iban);
        Double currentBalance = accountDTO.getBalance();
        String currentCurrency = accountDTO.getCurrency();
        Double actualBalance = accountService.getCurrencyValue(currentCurrency) * currentBalance;
        accountDTO.setBalance(actualBalance);
        return accountDTO;
    }
} 

帐户服务:

public class AccountService {

    @Autowired
    AccountRepository accountRepository;

    @Autowired
    RequestBuilder requestBuilder;

    @Autowired
    CurrencyDTO currencyDTO;

    @Cacheable("exchangeRates")
    public Double getCurrencyValue(String currency) {

        return currencyDTO.getCurrencyValue(currency);

    }

    public AccountDTO getAccountByIban(String iban){

        AccountDTO accountDTO = null;
        AccountEntity accountEntity = accountRepository.findByIban(iban);

        if (accountEntity != null ) {
            accountDTO = accountEntity.toDTO();
        }

        return accountDTO;
    }
}

单元测试类:

@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
public class AccountServiceTest {


    @MockBean
    AccountController accountController;

    @Spy
    @InjectMocks
    AccountService accountService;

    @Mock
    AccountRepository accountRepository;

    @Autowired
    MockMvc mockMvc;

    @Before
    public void setup() throws Exception {
        AccountEntity accountEntity = new AccountEntity();
        accountEntity.setIban("RO06PORL4513558777471397");
        accountEntity.setBalance(10000D);
        accountEntity.setCurrency("RON");

        Mockito.when(accountRepository.save(accountEntity)).thenReturn(accountEntity);
    }

    @Test
    public void testAccountNotFound() throws Exception {

        mockMvc.perform(get("/account/RO2E2"))
                .andExpect(status().isNotFound());
        //Assert.assertTrue(accountService.getAccountByIban("RO06PORL4513558777471397") == null);

AccountDTO:

public class AccountDTO {

    @ApiModelProperty(notes = "Account ID",name="id",required=true,value="id")
    private int id;

    @ApiModelProperty(notes = "Account IBAN",name="iban",required=true,value="iban")
    private String iban;

    @ApiModelProperty(notes = "Account Currency",name="currency",required=true,value="currency")
    private String currency;

    @ApiModelProperty(notes = "Account Balance",name="balance",required=true,value="balance")
    private Double balance;

    @ApiModelProperty(notes = "Last Update",name="lastUpdate",required=true,value="lastUpdate")
    private Date lastUpdate;

谁能帮我改正我的测试课?我看不出我做错了什么

【问题讨论】:

  • 在此处添加断点 AccountEntity accountEntity = accountRepository.findByIban(iban); accountEntitiy 是 null 还是别的什么?
  • 我有使用 C# 的 Moq 的经验,而不是 Java 的 Mockito,但是当您不指定行为时,自动模拟设置会返回什么?您是否需要手动告诉它默认返回 404,并且只为您在设置中指定的 iban 返回 200?
  • 请贴出 AccountNotFoundException 类的代码。

标签: java spring-boot unit-testing spring-mvc mockito


【解决方案1】:

你颠倒了 Controller 和 Service 的概念,嘲讽太多,混淆了测试框架。

当你想测试一个控制器的结果时,你应该对真正的控制器使用 MockMvc(在 ControllerTest 类中,而不是在 ServiceTest 类中),然后模拟 Service 被控制器用来让它返回你想要的东西给控制器。

类似:

import static org.mockito.Mockito.when;

@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
// @SpringBootTest 
// Not needed the whole context here, just use the faster way:
@WebMvcTest(AccountControllerTest.class)
public class AccountControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    AccountService accountService;    

    @Test
    public void testAccountFound() throws Exception {
        when(accountService.getAccountByIban("123"))
          .thenReturn(new AccountDTO(...));

        mockMvc.perform(get("/account/123"))
                .andExpect(status().isOk());
    }

    @Test
    public void testAccountNotFound() throws Exception {
        when(accountService.getAccountByIban("123"))
          .thenReturn(null);

        mockMvc.perform(get("/account/123"))
                .andExpect(status().isNotFound());
    }

}

那么,当你需要在一个ServiceTest类中测试Service的时候,你就不会使用MockMvc了,因为你要测试的是Service和Repository之间的通信,而不是Controller和Service之间的通信。始终跟随潮流。 在那里,您将模拟存储库,并使用不同的(模拟的)存储库结果测试 Service 输出正确的结果。


也就是说,当方法的返回类型是可选时,你应该使用 Optional 作为返回类型,而不是 null 然后在外部使用 Optional,因此:

public Optional<AccountDTO> getAccountByIban(String iban){

    AccountEntity accountEntity = accountRepository.findByIban(iban);

    if (accountEntity != null ) {
        return Optional.of(accountEntity.toDTO());
    }

    return Optional.empty();
}

和

@ApiOperation(value = "Get account with given iban ", response = AccountEntity.class)
@RequestMapping(value = "/account/{iban}", method = RequestMethod.GET )
public AccountDTO getAccountByIban(@PathVariable("iban") String iban ) throws AccountNotFoundException {

    return accountService.getAccountByIban(iban)
             .orElseThrow(() -> new AccountNotFoundException("Requested account was not found! [ iban = " + iban + "]"));
}

和

when(accountService.getAccountByIban("123"))
  .thenReturn(Optional.of(new AccountDTO(...)));

when(accountService.getAccountByIban("123"))
  .thenReturn(Optional.empty());

还要确保 AccountNotFoundException 用

注释
@ResponseStatus(HttpStatus.NOT_FOUND)

【讨论】:

  • 非常感谢所有的解释,它成功了!!
【解决方案2】:

您得到了奇怪的结果,因为您正在尝试使用mockMvc 测试您的服务,这对于单元测试控制器或进行一些集成测试很有帮助。似乎模拟您的控制器 bean 会导致始终返回 200 状态。

我看到的是您将单元测试与集成测试混合在一起,因为您正在使用 @SpringBootTest 以及存储库模拟。

我建议将您的测试转换为单元,并且不要在控制器中抛出异常。您的accountService 可能会返回Optional&lt;AccountDto&gt;,然后您的控制器可能会返回ResponseEntity&lt;AccountDto&gt;。然后您的控制器方法中的代码可能如下所示:

    return dto.map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());

并且你在你的测试设置中,你可以用空的可选来模拟你的服务。

此外,如果您想使用集成测试,那么您可以使用嵌入式 H2 数据库,并在测试之前将一些数据插入到您的数据库中。

【讨论】:

  • 这不能回答问题。为什么他/她得到 200?
  • 我做了更改,但我仍然得到 200 :(
  • @Maria1995 发生这种情况是因为您在模拟控制器。尝试为控制器和模拟服务创建单元测试,然后使用 mockmvc 检查端点。如果您想测试您的服务,则只模拟存储库并直接测试服务方法
猜你喜欢
  • 2021-04-04
  • 1970-01-01
  • 2016-08-18
  • 2019-08-01
  • 1970-01-01
  • 2018-06-22
  • 2016-02-10
  • 1970-01-01
  • 2013-05-17
相关资源
最近更新 更多