整体代码示例
首先,为了简化,我们让服务层就是简单的类,然后使用Id查找用户,这个和之前测试UserService接口不太一样哦:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
现在,我们要模拟UserRepository
的行为,使其在尝试获取用户时引发一个异常。这里我们使用Mockito进行模拟:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
//之前我们是定义了一个UserService接口,现在简化成UserService类了哈
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
//重点,后文详解!
@Test(expected = DatabaseConnectionException.class)
public void testGetUserByIdWithDbError() {
when(userRepository.findById(anyLong())).thenThrow(new DatabaseConnectionException("Database connection failed!"));
userService.getUserById(1L);
}
}
//重点,后文详解!
class DatabaseConnectionException extends RuntimeException {
public DatabaseConnectionException(String message) {
super(message);
}
}
在上述测试中,我们模拟了userRepository.findById()
方法,使其抛出DatabaseConnectionException
异常。然后,我们在测试方法上使用@Test(expected = DatabaseConnectionException.class)
来表示我们期望该方法引发此异常。
这样,如果getUserById
方法在遇到此异常时没有正确处理,测试将失败。这确保了即使在面对意外的数据库问题时,我们的代码仍能按预期的方式运行(在这种情况下,按预期抛出异常)。
到底在模拟什么?到底在测试什么?
下面,我们进一步说明:
综上所述,这个测试确保了当底层UserRepository
出现数据库连接错误时,上层的UserService
可以正确地传递这个错误。这对于后续的异常处理很重要,例如:在Controller层将这个异常转化为一个友好的错误消息返回给用户。
什么时候测试失败?
但是,以下几种情况可能导致测试不通过:
-
异常被吞没:如果
Service
层调用了Repository
的方法,但内部捕获了该异常并没有重新抛出,那么测试就会失败。例如:public User getUserById(Long id) { try { return userRepository.findById(id); } catch (DatabaseConnectionException e) { // 异常被吞没了 return null; } }
-
调用的方法不正确:如果
Service
层没有调用预期的Repository
方法,而是调用了其他方法,或者完全没有调用,那么模拟的异常就不会被触发,导致测试失败。 -
模拟的不正确:如果在测试中模拟的方法或参数与实际调用的方法或参数不匹配,那么模拟的异常也不会被触发。例如,如果
Service
实际上是这样调用的:userRepository.findById(2L)
,但我们的模拟是这样的:when(userRepository.findById(1L))...
,那么异常就不会被触发。-
其他未预料到的异常:有时可能会有其他的未被预料到的异常被抛出,这也会导致测试失败。
-
因此,虽然大多数情况下,如果Repository
层方法抛出了异常,Service
层应该也会抛出,但还是存在一些情况导致测试不通过,这也是进行此类测试的原因。
Exception 异常类定义
class DatabaseConnectionException extends RuntimeException {
public DatabaseConnectionException(String message) {
super(message);
}
}
这里,我们定义了一个继承自RuntimeException
的新异常类DatabaseConnectionException
。RuntimeException
是Java中所有非检查型异常的基类。所谓“非检查型”是指编译器不强制我们捕获或声明它。这与Exception
(检查型异常)相对。
关于DatabaseConnectionException
类的解释:
这种自定义异常,通常在我们希望为特定的错误情况定义更具描述性的异常名时使用,或者当我们想为特定的异常情况添加更多上下文信息时使用,信息越多,测试反馈的效果越好,所以一般使用自定义异常,继承RuntimeException!下面我们讨论一下,为什么建议使用RuntimeException?
RuntimeException 使用意义
下面是一些选择使用RuntimeException
的原因:
然而,这并不意味着总是应该选择非检查型异常。有时,如果你希望调用者必须处理某个特定的异常,使用检查型异常可能更合适。选择使用哪种异常是基于特定上下文和需求的决策。但在许多现代Java应用程序中,倾向于使用RuntimeException
因为它提供了更大的灵活性和简洁性。
总结
模拟异常的目的
真正的数据库异常是不是Runtime异常
在Java中,数据库操作可能会抛出多种异常。其中,SQLException
是一个受检异常(checked exception)。
但在很多现代的框架中(如Spring),这些受检异常通常会被转换成运行时异常(runtime exceptions),这样可以使代码更为简洁,避免了过多的try-catch
块。