问题描述
我在Maven项目上使用Mockito / JUnit,用于具有DAO设计模式的基于控制台的应用程序。我的IDE是Spring Toools Suite 3。
问题是,每次在特定的JUnit测试上运行此特定测试时,我都会得到一个UnfinishedStubbingException
,但我不知道为什么这样做,因为语法看起来正确。我对单元测试和Mockito还是很陌生,但是我认为这是发生的,因为从一层到下一层的抽象级别由于某种原因使Mockito感到困惑。因此,在测试用例中,我最初尝试在服务对象上使用 Spy 与 Mock (但随后会抛出NotAMockException
)。 / p>
对于解决此问题的任何建议和/或建议,将不胜感激。
这是堆栈跟踪:
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$1.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$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runchildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.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
类:
@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
类:
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
类:
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,那么您只是在模拟一个帐号为5的电话
,在这里进行模拟似乎有些混乱。
有了代码,您将希望为AccountServiceImpl
编写一个测试,但要模拟AccountDao
。您可以使用模拟DAO来设置它,以在调用某些方法时返回某些值,而不是使用与数据库对话的真实DAO。您还可以查询该模拟,以找出该模拟被调用了多少次以及具有什么值。
模拟域对象
在测试中,您似乎还选择了模拟Account
类。您没有在问题中加入Account
类,但我想它只是包含getter和setter。如果是这样,我不会打扰它。与其使用模拟帐户,不如使用真实帐户。替换行
Account account = Mockito.mock(Account.class);
使用
Account account = new Account();
这样,您可以在account
上调用getter和setter,它们的行为将与您期望的一样。如果您坚持使用模拟帐户,则必须使用Mockito来实现getter和setter:
when(account.getId()).thenReturn(...)
when(account.getBalance()).thenReturn(...)
// ...
您还可以在模拟帐户上致电account.setId()
。这将无济于事,因为在模拟帐户上致电.setId()
时,您没有告诉Mockito该怎么做。在真实帐户上致电account.setId()
会设置ID。
除了要测试的课程外,您不必模拟其他所有课程。是否执行取决于这些其他类的功能。如果另一个类具有某种复杂的逻辑,或者与数据库,文件系统,网络等进行通信,则可以将其模拟出来,这样您的测试就不必担心复杂的逻辑或与外部的通信。系统。但是,在这种情况下,我们不要打扰Account
,因为它不会做任何复杂的事情。
模拟设置
创建模拟时,所有void
方法将不执行任何操作,而所有其他方法将视情况返回false
,零或null
。如果要他们做其他事情,则需要设置它们。
您的服务使用您的DAO从数据库中加载帐户。您需要将模拟accountDao
设置为在给定ID后返回account
。为此,请在调用service.makeDeposit(...)
的行之前在测试中添加以下行:
when(daoMock.selectAccountByAccountId(1)).thenReturn(account);
但是updateAccountBalance()
方法呢?默认情况下,它什么都不做,您似乎正在尝试将其设置为不做任何事,而这已经做了。您可以删除尝试设置此方法的行,因为它将无法实现任何操作。稍后,我们将研究验证此方法,即断言它已被调用。
在模拟中进行模拟
此行出现错误:
doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount,accountNumber)
设置一个模拟时,不能在另一个模拟上调用方法。为什么要这样?如果是模拟,那么您应该已经设置了模拟方法以返回某个值,因此只需使用该值即可。换句话说,不是写作
// 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
也是模拟的,则将导致问题。
引起问题的原因是由于Mockito的工作方式。 Mockito看不到您的源代码,它看到的只是对它自己的静态方法的调用和对模拟的调用。线
doNothing().when(daoMock).updateAccountBalance(account.getBalance() + amount,accountNumber)
导致以下互动序列:
- 调用了静态Mockito方法
doNothing()
调用对象 -
doNothing()
方法, -
account.getBalance()
方法被调用,
模拟的DAO的 -
updateAccountBalance()
方法。 (在评估完所有参数之前,我们无法调用方法。)
when()
的对于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
方法。确实不需要进行验证:您可以清楚地看到此方法在上面称为三行。
通常,您不会在要测试的类上验证方法。相反,您可以验证类调用的模拟中的方法。相反,您要做的是验证模拟DAO上的相关方法是否已调用了预期值:
verify(daoMock,times(1)).updateAccountBalance(account.getBalance() + amount,account.getAccountId());
您也可以摆脱对Mockito.spy(...)
的呼叫。我自己从未使用过间谍,在这里也看不到需要间谍。替换行
service = Mockito.spy(new AccountServiceImpl());
使用
service = new AccountServiceImpl();
总结
这里有很多,希望至少有一部分是有意义的,并且可以使您更好地了解正在发生的事情。我对您的测试进行了上述更改,并使其通过。