错误和异常处理是测试中非常重要的部分。假设我们有一个服务,该服务从数据库中获取用户。现在,我们要考虑的错误场景是:数据库连接断开。
整体代码示例
首先,为了简化,我们让服务层就是简单的类,然后使用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.findById()
方法抛出DatabaseConnectionException
异常时,userService.getUserById()
方法也会抛出同样的异常。- 模拟异常:在这行代码中,我们指定了当
userRepository.findById()
被调用时,它应该抛出DatabaseConnectionException
异常。when(userRepository.findById(anyLong())).thenThrow(new DatabaseConnectionException("Database connection failed!"));
- 调用Service方法:接下来,我们调用了
userService.getUserById(1L)
。我们期望它在内部调用userRepository.findById()
(这在实际的UserService
实现中应该是这样的)。因此,由于我们已经模拟了userRepository.findById()
来抛出异常,所以userService.getUserById()
也应该会抛出这个异常。- 验证异常:
@Test(expected = DatabaseConnectionException.class)
注解表示我们期望这个测试方法在执行时会抛出DatabaseConnectionException
异常。如果这个方法执行完并没有抛出这个异常,那么测试将会失败。- 测试的目的:这个测试的目的并不是检查
userRepository.findById()
本身是否真的会抛出异常,而是检查当它抛出异常时,userService.getUserById()
是否会正确地传递这个异常。这可以帮助我们确保UserService
在处理异常时的行为是正确的。****其实本质上来说,抛出异常和预期值的测试逻辑几乎是一样的,都是通过给定下层值,验证上层代码关系。
综上所述,这个测试确保了当底层
UserRepository
出现数据库连接错误时,上层的
UserService
可以正确地传递这个错误。这对于后续的异常处理很重要,例如:在Controller层将这个异常转化为一个友好的错误消息返回给用户。
什么时候测试失败?
在正常情况下,只要
Service
层确实调用了
Repository
的方法,并且
Repository
的方法抛出了
RuntimeException
(或其子类),那么
Service
层的调用方法也应该会收到并进一步抛出这个异常。
但是,以下几种情况可能导致测试不通过:
- 异常被吞没:如果
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);
}
}
DatabaseConnectionException
是一个自定义的异常类。在Java中,异常是用来表示程序运行中的问题或异常情况的对象。当某些问题发生时,通常会抛出(throw)一个异常。
这里,我们定义了一个继承自
RuntimeException
的新异常类
DatabaseConnectionException
。**
RuntimeException
是Java中所有非检查型异常的基类。所谓“非检查型”是指编译器不强制我们捕获或声明它**。这与
Exception
(检查型异常)相对。
关于
DatabaseConnectionException
类的解释:
class DatabaseConnectionException extends RuntimeException
- 这表示我们正在定义一个名为DatabaseConnectionException
的新类,该类是RuntimeException
的子类。这意味着DatabaseConnectionException
继承了RuntimeException
的所有特性。public DatabaseConnectionException(String message)
- 这是DatabaseConnectionException
类的构造方法。当我们创建DatabaseConnectionException
的新实例时,可以传递一个消息字符串给这个构造函数。**super(message)**;
- 这行代码调用了父类(RuntimeException
)的构造方法,并将message
传递给它。这样,当异常被抛出并捕获时,我们可以获取并显示这个消息。
这种自定义异常,通常在我们希望为特定的错误情况定义更具描述性的异常名时使用,或者当我们想为特定的异常情况添加更多上下文信息时使用,信息越多,测试反馈的效果越好,所以一般使用自定义异常,继承RuntimeException!下面我们讨论一下,为什么建议使用RuntimeException?
RuntimeException 使用意义
使用
RuntimeException
(非检查型异常)还是
Exception
(检查型异常)来自定义数据库异常(或其他异常)是一个设计决策,并且这两者在Java中有不同的含义和用途。
下面是一些选择使用
RuntimeException
的原因:
- 不需要显式处理:当方法中抛出非检查型异常时,调用该方法的代码不需要显式地处理异常(即不需要使用
try-catch
或在方法签名中使用throws
)。这使得代码更简洁,更易读。- 表示编程错误:非检查型异常通常用于表示编程错误,例如空指针异常、数组越界等。对于某些数据库异常,如配置错误,这可能是一个编程错误,因此使用
RuntimeException
可能更合适。- 强制开发者考虑异常处理策略:使用检查型异常会强制调用者处理异常,这可能会导致过多的
try-catch
块并使代码复杂化。而使用非检查型异常,开发者可以选择在何处处理异常,这通常会导致更好、更集中的异常处理策略。- 与现有框架兼容:许多现代Java框架,如Spring,倾向于使用非检查型异常,因为它们认为异常应该在应用程序的高层(如Controller或Service)中统一处理。
- 灵活性:有时,在开发过程的后期,可能会发现某些异常不再是关键的,不需要强制处理。对于非检查型异常,这意味着不需要修改方法签名或调用代码。
然而,这并不意味着总是应该选择非检查型异常。有时,如果你希望调用者必须处理某个特定的异常,使用检查型异常可能更合适。选择使用哪种异常是基于特定上下文和需求的决策。但在许多现代Java应用程序中,倾向于使用
RuntimeException
因为它提供了更大的灵活性和简洁性。
总结
模拟异常的目的
- 验证代码在遇到异常时是否有正确的响应,例如是否抛出了预期的异常。
- 确保代码在异常情况下仍然能够维持预期的状态或行为。
- 单元测试通常关注隔离性,因此模拟异常可以确保在不涉及实际外部依赖的情况下,模拟各种可能的场景。
真正的数据库异常是不是Runtime异常
在Java中,数据库操作可能会抛出多种异常。其中,
SQLException
是一个受检异常(checked exception)。
但在很多现代的框架中(如Spring),这些受检异常通常会被转换成运行时异常(runtime exceptions),这样可以使代码更为简洁,避免了过多的
try-catch
块。
版权归原作者 Joy T 所有, 如有侵权,请联系我们删除。