在调用 void ServiceImplementationLayer 方法的 JUnit 测试用例上抛出 Mockito UnfinishedStubbingException

Mockito UnfinishedStubbingException thrown on a JUnit test case which calls a void ServiceImplementationLayer method

我正在 Maven 项目上使用 Mockito/JUnit,用于基于控制台的应用程序,采用 DAO 设计模式。我的 IDE 是 Spring Tools Suite 3.

问题是,每次我在我的特定 JUnit 测试中 运行 这个特定测试时,我都会得到一个 UnfinishedStubbingException,我不知道为什么会这样,因为语法看起来是正确的。我对单元测试和 Mockito 非常陌生,但我认为这是因为从一层到下一层的抽象级别由于某种原因混淆了 Mockito。因此,我最初尝试在测试用例中的服务对象上使用 Spy 而不是 Mock (但 NotAMockException 是而是被抛出)。

任何建议 and/or 如何解决此问题的建议将不胜感激。

这是堆栈跟踪:

org.mockito.exceptions.misusing.UnfinishedStubbingException: 
Unfinished stubbing detected here:
-> at com.revature.testing.BankAccountEvaluationService.testMakeDeposit_ValidUserId(BankAccountEvaluationService.java:91)

E.g. thenReturn() may be missing.
Examples of correct stubbing:
    when(mock.isOk()).thenReturn(true);
    when(mock.isOk()).thenThrow(exception);
    doThrow(exception).when(mock).someVoidMethod();
Hints:
 1. missing thenReturn()
 2. you are trying to stub a final method, which is not supported
 3: you are stubbing the behaviour of another mock inside before 'thenReturn' instruction if completed

    at com.revature.testing.BankAccountEvaluationService.testMakeDeposit_ValidUserId(BankAccountEvaluationService.java:91)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=10=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)

示例代码如下:

BankAccountEvaluationTest class:

@InjectMocks
private AccountServiceImpl service;

@Mock
private AccountDaoImpl daoMock;

@Before
public void setUp() {
    service = Mockito.spy(new AccountServiceImpl());
    MockitoAnnotations.initMocks(this);
}

@Test
public void testMakeDeposit_ValidUserId() {
    //setup
    Account account = Mockito.mock(Account.class);
    account.setAccountId(1);
    double amount = 20.50;
    //gives UnfinishedStubbingException -> Mockito doesn't like this because it's mocking within a mocking object
    //doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount, accountNumber); //Solution?
    
    //run
    service.makeDeposit(amount, account.getAccountId());
    
    //verify
    verify(service, times(1)).makeDeposit(amount, account.getAccountId());
}

AccountServiceImpl class:

package com.revature.serviceimpl;
import java.util.List;

import org.apache.log4j.Logger;
import com.revature.dao.AccountDao;
import com.revature.daoimpl.AccountDaoImpl;
import com.revature.model.Account;
import com.revature.service.AccountService;

public class AccountServiceImpl implements AccountService {
    private static Logger logger = Logger.getLogger(AccountServiceImpl.class);
    private AccountDao accountDao = new AccountDaoImpl();
    
    //other overridden methods from AccountService interface

    @Override
    public void makeDeposit(double addedCash, int id) {
        logger.info("Sending deposit request to the database.");
        // find the account
        Account account = accountDao.selectAccountByAccountId(id);
        System.out.println(account);
        // set new balance
        account.setBalance(account.getBalance() + addedCash);
        double myNewBalance = account.getBalance();
        logger.info("New balance: " + account.getBalance());
        logger.info("Updating account balance to account number " + id);
        // update the database
        accountDao.updateAccountBalance(myNewBalance, id);
    }
}

AccountDaoImpl class:

package com.revature.daoimpl;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;

import com.revature.dao.AccountDao;
import com.revature.model.Account;
import com.revature.model.AccountStatus;
import com.revature.model.AccountType;

public class AccountDaoImpl implements AccountDao {
    private static Logger logger = Logger.getLogger(UserDaoImpl.class);

    private static String url = MY_URL;
    private static String dbUsername = MY_DATABASE_NAME;
    private static String dbPassword = MY_DATABASE_PASSWORD;

    //other overridden methods from AccountDao interface

    @Override
    public void updateAccountBalance(double balance, int id) {
        try (Connection conn = DriverManager.getConnection(url, dbUsername, dbPassword)) {

            String sql = "UPDATE accounts SET account_balance = ?  WHERE account_id = ?;";
            PreparedStatement ps = conn.prepareStatement(sql);

            ps.setDouble(1, balance);
            ps.setInt(2, id);
            ps.executeUpdate();
            logger.info("new balance is now set");
        } catch (SQLException e) {
            logger.warn("Error in SQL execution to update balance. Stack Trace: ", e);
        }
    }
}

自从我上次使用 Mockito 以来已经有一段时间了,但它可以代替

doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount, accountNumber); 

你应该使用

doNothing().when(daoMock).updateAccountBalance(Mockito.any(), Mockito.any()); 

...也许你也可以试试

when(daoMock.updateAccountBalance(Mockito.any(), Mockito.any())).doNothing();

我认为(但我不确定)您只是在用参数 (account.getBalance() + amount, accountNumber)

模拟确切的调用

例如如果您在设置模拟时的帐号是 5,那么您只是在模拟 accountnumber = 5

的单个呼叫

这里的模拟似乎有些混乱。

使用您拥有的代码,您可能希望为 AccountServiceImpl 编写测试,但模拟 AccountDao。您不使用与数据库对话的真实 DAO,而是使用模拟 DAO,您可以在调用某些方法时将其设置为 return 某些值。您还可以查询模拟以了解它被调用了多少次以及调用了哪些值。

模拟域对象

在您的测试中,您似乎还选择了模拟 Account class。您没有在问题中包含 Account class 但我猜它只有 getter 和 setter。如果是这样,我不会费心去嘲笑它。与其使用模拟账户,不如使用真实账户。替换行

        Account account = Mockito.mock(Account.class);

        Account account = new Account();

这样您就可以在 account 上调用 getter 和 setter,它们的行为将如您所愿。如果你坚持使用模拟账户,你将不得不使用 Mockito 来实现 getters 和 setters:

        when(account.getId()).thenReturn(...)        
        when(account.getBalance()).thenReturn(...)
        // ...

您还通过您的模拟账户致电 account.setId()。这不会执行任何操作,因为当您在模拟帐户上调用 .setId() 时,您还没有告诉 Mockito 要做什么。在真实帐户上调用 account.setId() 将设置 ID。

除了被测试的以外,您不必模拟每个 class。你是否这样做取决于其他 classes 做什么。如果另一个 class 有一些复杂的逻辑,或者与数据库、文件系统、网络等通信,那么模拟它会很有用,这样你的测试就不必担心复杂的逻辑或与外部系统的通信。然而,在这种情况下,我们不必费心去嘲笑 Account,因为它不会做任何复杂的事情。

模拟设置

创建模拟时,所有 void 方法将不执行任何操作,所有其他方法将 return false、零或 null 视情况而定。如果你想让他们做别的事情,你需要设置他们。

您的服务使用您的 DAO 从数据库加载帐户。给定 ID 后,您需要将 mock accountDao 设置为 return account。通过在调用 service.makeDeposit(...):

的行之前将以下行添加到测试中来执行此操作
        when(daoMock.selectAccountByAccountId(1)).thenReturn(account);

但是 updateAccountBalance() 方法呢?默认情况下,它什么都不做,你似乎试图将它设置为什么都不做,它已经这样做了。您可以删除尝试设置此方法的行,因为它什么也做不了。稍后,我们将查看验证此方法,即断言它已被调用。

模拟中的模拟

您收到此行的错误:

        doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount, accountNumber)

设置一个模拟时,您不能在另一个模拟上调用方法。你为什么需要?如果它是一个 mock,那么您应该已经将 mock 方法设置为 return 某个值,因此只需使用该值即可。换句话说,而不是写

        // if account is a mock...
        when(account.getBalance()).thenReturn(10.00);
        doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount, accountNumber)

随便写

        // if account is a mock...
        when(account.getBalance()).thenReturn(10.00);
        doNothing().when(daoMock).updateAccountBalance(10.00 + amount, accountNumber)

在这一行中,您正在尝试设置 daoMock 并正在调用 account.getBalance()。如果 account 也是 mock,这将导致问题。

导致问题的原因是 Mockito 的工作方式。 Mockito 看不到你的源代码,它看到的只是对其自身静态方法的调用和对 mock 的调用。行

        doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount, accountNumber)

导致以下交互序列:

  1. 已调用静态 Mockito 方法 doNothing()
  2. when() 任何对象 doNothing() 调用的方法,
  3. account.getBalance() 方法调用,
  4. updateAccountBalance() 调用了模拟 DAO 的方法。 (在计算完所有参数之前,我们不能调用方法。)

对于 Mockito,前三个步骤与以下没有区别:

        doNothing().when(daoMock);
        when(account.getBalance()).thenReturn(...);

在这种情况下,很明显我们还没有完成设置daoMock。我们希望在这种情况下出现异常。

Mockito 的语法使模拟变得清晰和富有表现力,但如果不小心,您有时可能会遇到这样的情况。

我们已经决定删除​​导致此问题的行,因此本节更多的是提供信息和理解。

验证

接下来,我们看下面几行:

        //run
        service.makeDeposit(amount, account.getAccountId());

        //verify
        verify(service, times(1)).makeDeposit(amount, account.getAccountId());

这是做什么的?您调用 makeDeposit 方法,然后 您验证您调用了 makeDeposit 方法 。这个验证真的不用了,上面三行就可以清楚的看到这个方法被调用了

通常,您不会验证正在测试的 class 上的方法。相反,您在 class 调用的模拟上验证方法。您要做的是验证是否使用预期值调用了模拟 DAO 上的相关方法:

        verify(daoMock, times(1)).updateAccountBalance(account.getBalance() + amount, account.getAccountId());

您也可以取消对 Mockito.spy(...) 的调用。我自己从来没有使用过间谍,而且我认为这里不需要它们。替换行

        service = Mockito.spy(new AccountServiceImpl());

        service = new AccountServiceImpl();

总之

这里有相当多的内容,希望其中至少有一部分是有意义的,可以让您更好地理解正在发生的事情。我对您的测试进行了上述更改并通过了测试。