錯誤和異常處理是測試中非常重要的部分。假設(shè)我們有一個服務(wù),該服務(wù)從數(shù)據(jù)庫中獲取用戶?,F(xiàn)在,我們要考慮的錯誤場景是:數(shù)據(jù)庫連接斷開。
整體代碼示例
首先,為了簡化,我們讓服務(wù)層就是簡單的類,然后使用Id查找用戶,這個和之前測試UserService接口不太一樣哦:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
現(xiàn)在,我們要模擬UserRepository
的行為,使其在嘗試獲取用戶時引發(fā)一個異常。這里我們使用Mockito進行模擬:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
//之前我們是定義了一個UserService接口,現(xiàn)在簡化成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)
來表示我們期望該方法引發(fā)此異常。
這樣,如果getUserById
方法在遇到此異常時沒有正確處理,測試將失敗。這確保了即使在面對意外的數(shù)據(jù)庫問題時,我們的代碼仍能按預(yù)期的方式運行(在這種情況下,按預(yù)期拋出異常)。
到底在模擬什么?到底在測試什么?
下面,我們進一步說明:
測試目標:這個測試的目標是確保當
userRepository.findById()
方法拋出DatabaseConnectionException
異常時,userService.getUserById()
方法也會拋出同樣的異常。模擬異常:在這行代碼中,我們指定了當
userRepository.findById()
被調(diào)用時,它應(yīng)該拋出DatabaseConnectionException
異常。when(userRepository.findById(anyLong())).thenThrow(new DatabaseConnectionException("Database connection failed!"));
調(diào)用Service方法:接下來,我們調(diào)用了
userService.getUserById(1L)
。我們期望它在內(nèi)部調(diào)用userRepository.findById()
(這在實際的UserService
實現(xiàn)中應(yīng)該是這樣的)。因此,由于我們已經(jīng)模擬了userRepository.findById()
來拋出異常,所以userService.getUserById()
也應(yīng)該會拋出這個異常。驗證異常:
@Test(expected = DatabaseConnectionException.class)
注解表示我們期望這個測試方法在執(zhí)行時會拋出DatabaseConnectionException
異常。如果這個方法執(zhí)行完并沒有拋出這個異常,那么測試將會失敗。測試的目的:這個測試的目的并不是檢查
userRepository.findById()
本身是否真的會拋出異常,而是檢查當它拋出異常時,userService.getUserById()
是否會正確地傳遞這個異常。這可以幫助我們確保UserService
在處理異常時的行為是正確的。其實本質(zhì)上來說,拋出異常和預(yù)期值的測試邏輯幾乎是一樣的,都是通過給定下層值,驗證上層代碼關(guān)系。
綜上所述,這個測試確保了當?shù)讓?code>UserRepository出現(xiàn)數(shù)據(jù)庫連接錯誤時,上層的UserService
可以正確地傳遞這個錯誤。這對于后續(xù)的異常處理很重要,例如:在Controller層將這個異常轉(zhuǎn)化為一個友好的錯誤消息返回給用戶。
什么時候測試失?。?/h2>
在正常情況下,只要Service
層確實調(diào)用了Repository
的方法,并且Repository
的方法拋出了RuntimeException
(或其子類),那么Service
層的調(diào)用方法也應(yīng)該會收到并進一步拋出這個異常。
在正常情況下,只要Service
層確實調(diào)用了Repository
的方法,并且Repository
的方法拋出了RuntimeException
(或其子類),那么Service
層的調(diào)用方法也應(yīng)該會收到并進一步拋出這個異常。
但是,以下幾種情況可能導(dǎo)致測試不通過:
-
異常被吞沒:如果
Service
層調(diào)用了Repository
的方法,但內(nèi)部捕獲了該異常并沒有重新拋出,那么測試就會失敗。例如:public User getUserById(Long id) { try { return userRepository.findById(id); } catch (DatabaseConnectionException e) { // 異常被吞沒了 return null; } }
-
調(diào)用的方法不正確:如果
Service
層沒有調(diào)用預(yù)期的Repository
方法,而是調(diào)用了其他方法,或者完全沒有調(diào)用,那么模擬的異常就不會被觸發(fā),導(dǎo)致測試失敗。 -
模擬的不正確:如果在測試中模擬的方法或參數(shù)與實際調(diào)用的方法或參數(shù)不匹配,那么模擬的異常也不會被觸發(fā)。例如,如果
Service
實際上是這樣調(diào)用的:userRepository.findById(2L)
,但我們的模擬是這樣的:when(userRepository.findById(1L))...
,那么異常就不會被觸發(fā)。-
其他未預(yù)料到的異常:有時可能會有其他的未被預(yù)料到的異常被拋出,這也會導(dǎo)致測試失敗。
-
因此,雖然大多數(shù)情況下,如果Repository
層方法拋出了異常,Service
層應(yīng)該也會拋出,但還是存在一些情況導(dǎo)致測試不通過,這也是進行此類測試的原因。
Exception 異常類定義
class DatabaseConnectionException extends RuntimeException {
public DatabaseConnectionException(String message) {
super(message);
}
}
DatabaseConnectionException
是一個自定義的異常類。在Java中,異常是用來表示程序運行中的問題或異常情況的對象。當某些問題發(fā)生時,通常會拋出(throw)一個異常。
這里,我們定義了一個繼承自RuntimeException
的新異常類DatabaseConnectionException
。RuntimeException
是Java中所有非檢查型異常的基類。所謂“非檢查型”是指編譯器不強制我們捕獲或聲明它。這與Exception
(檢查型異常)相對。
關(guān)于DatabaseConnectionException
類的解釋:
class DatabaseConnectionException extends RuntimeException
- 這表示我們正在定義一個名為DatabaseConnectionException
的新類,該類是RuntimeException
的子類。這意味著DatabaseConnectionException
繼承了RuntimeException
的所有特性。
public DatabaseConnectionException(String message)
- 這是DatabaseConnectionException
類的構(gòu)造方法。當我們創(chuàng)建DatabaseConnectionException
的新實例時,可以傳遞一個消息字符串給這個構(gòu)造函數(shù)。
super(message);
- 這行代碼調(diào)用了父類(RuntimeException
)的構(gòu)造方法,并將message
傳遞給它。這樣,當異常被拋出并捕獲時,我們可以獲取并顯示這個消息。
這種自定義異常,通常在我們希望為特定的錯誤情況定義更具描述性的異常名時使用,或者當我們想為特定的異常情況添加更多上下文信息時使用,信息越多,測試反饋的效果越好,所以一般使用自定義異常,繼承RuntimeException!下面我們討論一下,為什么建議使用RuntimeException?
RuntimeException 使用意義
使用
RuntimeException
(非檢查型異常)還是Exception
(檢查型異常)來自定義數(shù)據(jù)庫異常(或其他異常)是一個設(shè)計決策,并且這兩者在Java中有不同的含義和用途。
下面是一些選擇使用RuntimeException
的原因:
不需要顯式處理:當方法中拋出非檢查型異常時,調(diào)用該方法的代碼不需要顯式地處理異常(即不需要使用
try-catch
或在方法簽名中使用throws
)。這使得代碼更簡潔,更易讀。表示編程錯誤:非檢查型異常通常用于表示編程錯誤,例如空指針異常、數(shù)組越界等。對于某些數(shù)據(jù)庫異常,如配置錯誤,這可能是一個編程錯誤,因此使用
RuntimeException
可能更合適。強制開發(fā)者考慮異常處理策略:使用檢查型異常會強制調(diào)用者處理異常,這可能會導(dǎo)致過多的
try-catch
塊并使代碼復(fù)雜化。而使用非檢查型異常,開發(fā)者可以選擇在何處處理異常,這通常會導(dǎo)致更好、更集中的異常處理策略。與現(xiàn)有框架兼容:許多現(xiàn)代Java框架,如Spring,傾向于使用非檢查型異常,因為它們認為異常應(yīng)該在應(yīng)用程序的高層(如Controller或Service)中統(tǒng)一處理。
靈活性:有時,在開發(fā)過程的后期,可能會發(fā)現(xiàn)某些異常不再是關(guān)鍵的,不需要強制處理。對于非檢查型異常,這意味著不需要修改方法簽名或調(diào)用代碼。
然而,這并不意味著總是應(yīng)該選擇非檢查型異常。有時,如果你希望調(diào)用者必須處理某個特定的異常,使用檢查型異??赡芨线m。選擇使用哪種異常是基于特定上下文和需求的決策。但在許多現(xiàn)代Java應(yīng)用程序中,傾向于使用RuntimeException
因為它提供了更大的靈活性和簡潔性。
總結(jié)
模擬異常的目的
- 驗證代碼在遇到異常時是否有正確的響應(yīng),例如是否拋出了預(yù)期的異常。
- 確保代碼在異常情況下仍然能夠維持預(yù)期的狀態(tài)或行為。
- 單元測試通常關(guān)注隔離性,因此模擬異常可以確保在不涉及實際外部依賴的情況下,模擬各種可能的場景。
真正的數(shù)據(jù)庫異常是不是Runtime異常
在Java中,數(shù)據(jù)庫操作可能會拋出多種異常。其中,SQLException
是一個受檢異常(checked exception)。文章來源:http://www.zghlxwxcb.cn/news/detail-717354.html
但在很多現(xiàn)代的框架中(如Spring),這些受檢異常通常會被轉(zhuǎn)換成運行時異常(runtime exceptions),這樣可以使代碼更為簡潔,避免了過多的try-catch
塊。文章來源地址http://www.zghlxwxcb.cn/news/detail-717354.html
到了這里,關(guān)于Junit 單元測試之錯誤和異常處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!