Giả sử bạn đang phát triển một ứng dụng. Sau nhiều giờ viết mã, bạn có thể tạo ra một số tính năng thú vị. Bây giờ, bạn muốn chắc chắn rằng các tính năng đang hoạt động như bạn muốn.
Điều này liên quan đến việc kiểm tra xem mỗi và mọi đoạn mã có hoạt động như mong đợi hay không. Quy trình này được gọi là Kiểm tra đơn vị. Các ngôn ngữ khác nhau cung cấp các khuôn khổ riêng để thử nghiệm.
Trong bài viết này, tôi sẽ chỉ cho bạn cách viết các bài kiểm tra đơn vị trong Java. Trước tiên, tôi sẽ giải thích những gì liên quan đến kiểm thử và một số khái niệm bạn cần biết. Sau đó, tôi sẽ đưa ra một vài ví dụ để giúp bạn hiểu rõ hơn.
Đối với bài viết này, tôi cho rằng bạn đã quen thuộc với Java và IntelliJ IDE.
Thiết lập dự án
Đối với dự án này, tôi sẽ sử dụng IntelliJ IDE. Nếu bạn không có nó, hãy làm theo hướng dẫn này để cài đặt IDE.
Trong dự án này, chúng tôi sẽ sử dụng thư viện JUnit và Mockito để thử nghiệm. Đây là những thư viện được sử dụng phổ biến nhất để thử nghiệm trong Java. Bạn sẽ hiểu cách các thư viện này được sử dụng khi xem qua bài viết.
Để thiết lập JUnit, hãy làm theo các bước sau như được mô tả trong hướng dẫn này :
- Từ menu chính, chọn Tệp > Mới > Dự án.
- Chọn Dự án mới. Chỉ định một tên cho dự án, tôi sẽ cung cấp cho nó hướng dẫn thử nghiệm junit.
- Chọn Maven làm công cụ xây dựng và trong ngôn ngữ, hãy chọn Java.
- Từ danh sách JDK , chọn JDK bạn muốn sử dụng trong dự án.
- Nhấp vào Tạo.
- Mở pom.xml trong thư mục gốc của dự án của bạn.
- Trong pom.xml, nhấn
⌘ + N
và chọn Thêm phần phụ thuộc. - Thao tác này sẽ mở một cửa sổ công cụ, nhập
org.junit.jupiter:junit-jupiter
vào trường tìm kiếm. Xác định vị trí phụ thuộc cần thiết và nhấp vào Thêm bên cạnh nó. - Bây giờ, hãy nhấp vào Tải các thay đổi Maven trong thông báo xuất hiện ở góc trên cùng bên phải trong trình chỉnh sửa.
Bây giờ, để thiết lập Mockito, hãy thêm hai phụ thuộc này vào pom.xml
:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.2.0</version>
<scope>compile</scope>
</dependency>
Lưu ý : Phiên bản có thể khác nhau tùy thuộc vào thời điểm bạn đọc bài đăng này.
Để hoàn tất thiết lập, hãy tạo một lớp Welcome
và xác định chức năng chính của bạn ở đó.
Kiểm tra đơn vị là gì?
Kiểm tra đơn vị liên quan đến việc kiểm tra từng thành phần trong mã của bạn để xem chúng có hoạt động như mong đợi hay không. Nó cô lập từng phương thức riêng lẻ trong mã của bạn và thực hiện các bài kiểm tra trên đó. Bài kiểm tra đơn vị giúp đảm bảo rằng phần mềm của bạn đang hoạt động như mong đợi trước khi phát hành.
Là một nhà phát triển, bạn sẽ viết các bài kiểm tra đơn vị ngay sau khi viết xong một đoạn mã. Bây giờ, bạn có thể hỏi, đó không phải là công việc của một tester sao? Theo một cách nào đó, vâng, người kiểm thử chịu trách nhiệm kiểm thử phần mềm. Tuy nhiên, bao gồm mọi dòng mã sẽ tạo thêm rất nhiều áp lực cho người kiểm tra. Vì vậy, cách tốt nhất là các nhà phát triển cũng nên viết các bài kiểm tra cho mã của riêng họ.
Mục tiêu của thử nghiệm đơn vị là đảm bảo rằng bất kỳ chức năng mới nào không phá vỡ chức năng hiện có. Nó cũng giúp xác định bất kỳ vấn đề hoặc lỗi nào sớm hơn trong quá trình phát triển và giúp đảm bảo rằng mã đáp ứng các tiêu chuẩn chất lượng do tổ chức đặt ra.
Những điều nên làm và không nên làm trong kiểm thử đơn vị
Hãy nhớ các nguyên tắc sau khi viết bài kiểm tra cho các phương pháp của bạn:
- Kiểm tra xem đầu ra dự kiến của một phương thức có khớp với đầu ra thực tế hay không.
- Kiểm tra xem các lệnh gọi hàm được thực hiện bên trong phương thức có xảy ra với số lần mong muốn hay không.
- Đừng thử kiểm tra mã không phải là một phần của phương pháp đang được kiểm tra.
- Không thực hiện lệnh gọi API, kết nối cơ sở dữ liệu hoặc yêu cầu mạng trong khi viết bài kiểm tra của bạn.
Bây giờ, hãy xem qua một số khái niệm bạn cần biết trước khi bắt tay vào viết bài kiểm tra.
khẳng định
Các xác nhận xác định xem bài kiểm tra của bạn có đạt hay không. Họ so sánh giá trị trả về dự kiến của một phương thức với giá trị thực tế. Có một số khẳng định bạn có thể đưa ra khi kết thúc bài kiểm tra của mình.
Lớp Assertions
trong JUnit bao gồm các phương thức tĩnh cung cấp các điều kiện khác nhau quyết định xem bài kiểm tra có đạt hay không. Chúng ta sẽ xem các phương pháp này khi tôi hướng dẫn bạn qua từng ví dụ.
Chế giễu
Lớp có các phương thức mà bạn đang thử nghiệm có thể có một số phụ thuộc bên ngoài. Như đã đề cập trước đây, bạn không nên thử kiểm tra mã không phải là một phần của chức năng được kiểm tra.
Nhưng trong trường hợp hàm của bạn sử dụng một lớp bên ngoài, thì đó là một cách tốt để giả định lớp đó – nghĩa là có các giá trị giả thay vì giá trị thực. Chúng tôi sẽ sử dụng thư viện Mockito cho mục đích này.
phương pháp khai thác
Các phụ thuộc bên ngoài có thể không chỉ giới hạn ở các lớp mà còn ở một số phương thức nhất định. Việc khai thác phương thức nên được thực hiện khi chức năng của bạn đang gọi một chức năng bên ngoài trong mã của nó. Trong trường hợp này, bạn làm cho hàm đó trả về giá trị bạn muốn thay vì gọi phương thức thực tế.
Chẳng hạn, phương thức bạn đang thử nghiệm (A) đang gọi một phương thức bên ngoài (B) trong quá trình triển khai. B thực hiện một truy vấn cơ sở dữ liệu, tìm nạp tất cả các sinh viên có điểm lớn hơn 80. Thực hiện một cuộc gọi cơ sở dữ liệu thực tế không phải là một phương pháp hay ở đây. Vì vậy, bạn khai thác phương thức và làm cho nó trả về một danh sách giả gồm các sinh viên mà bạn cần để kiểm tra.
Bạn sẽ hiểu điều này tốt hơn với các ví dụ. Có nhiều khái niệm khác là một phần của thử nghiệm trong Java. Tuy nhiên, hiện tại ba điều này là đủ để bạn bắt đầu.
Các bước để thực hiện trong khi thử nghiệm
- Khởi tạo các tham số cần thiết mà bạn sẽ cần để thực hiện kiểm tra.
- Tạo các đối tượng giả và khai thác bất kỳ phương thức nào nếu được yêu cầu.
- Gọi phương thức bạn đang thử nghiệm với các tham số bạn đã khởi tạo ở Bước 1.
- Thêm một xác nhận để kiểm tra kết quả kiểm tra của bạn. Điều này sẽ quyết định nếu bài kiểm tra vượt qua.
Bạn sẽ hiểu các bước này nhiều hơn với các ví dụ. Hãy bắt đầu với một bài kiểm tra cơ bản trước.
Cách viết bài kiểm tra đầu tiên
Hãy viết một hàm đơn giản để so sánh hai số. Nó trả về 1
nếu số đầu tiên lớn hơn số thứ hai và trả về -1
ngược lại.
Chúng ta sẽ đặt chức năng này bên trong một Basics
lớp:
public class Basics {
public int compare(int n1, int n2) {
if (n1 > n2) return 1;
return -1;
}
}
Khá đơn giản! Hãy viết bài kiểm tra cho lớp học này. Tất cả các bài kiểm tra của bạn nên được đặt bên trong thư mục kiểm tra.
Bên trong thư mục kiểm tra, hãy tạo một lớp BasicTests
nơi bạn sẽ viết các bài kiểm tra của mình cho lớp này. Tên của lớp học không quan trọng, nhưng cách tốt nhất là tách biệt các bài kiểm tra theo từng lớp. Ngoài ra, hãy làm theo cấu trúc thư mục tương tự như cấu trúc trong mã chính của bạn.
public class BasicTests {
// Your tests come here
}
Các bài kiểm tra đơn vị về cơ bản là một tập hợp các phương thức bạn xác định để kiểm tra từng phương thức trong lớp của bạn. Bên trong lớp trên, hãy tạo một phương thức compare()
có kiểu trả về là void
. Một lần nữa, bạn có thể đặt tên cho phương thức bất cứ điều gì bạn muốn.
@Test
public void compare() {
}
Chú @Test
thích chỉ ra rằng phương pháp này sẽ được chạy dưới dạng trường hợp thử nghiệm.
Bây giờ, để kiểm tra phương thức, bạn cần tạo đối tượng của lớp trên và gọi phương thức bằng cách truyền một số giá trị.
Basics basicTests = new Basics();
int value = basicTests.compare(2, 1);
Bây giờ, hãy sử dụng assertEquals()
phương thức của Assertions
lớp để kiểm tra xem giá trị dự kiến có khớp với giá trị dự kiến hay không.
Assertions.assertEquals(1, value);
Bài kiểm tra của chúng tôi sẽ vượt qua, vì giá trị được phương thức trả về khớp với giá trị dự kiến. Để kiểm tra, hãy chạy thử nghiệm bằng cách nhấp chuột phải vào mũi tên màu xanh lá cây bên cạnh phương pháp thử nghiệm.
Kết quả kiểm tra của bạn sẽ được hiển thị dưới đây.
Thêm ví dụ thử nghiệm
Trong thử nghiệm trên, chúng tôi chỉ thử nghiệm một kịch bản. Khi có sự phân nhánh trong hàm, bạn cần viết các bài kiểm tra cho từng điều kiện. Hãy giới thiệu thêm một số nhánh trong hàm trên.
public int compare(int n1, int n2) {
if (n1 > n2) return 1;
else if (n1 < n2) return -1;
return 0;
}
Chúng tôi đã kiểm tra nhánh đầu tiên, vì vậy hãy viết các bài kiểm tra cho hai nhánh còn lại.
@Test
@DisplayName("First number is less than the second")
public void compare2() {
Basics basicTests = new Basics();
int value = basicTests.compare(2, 3);
Assertions.assertEquals(-1, value);
}
Chú @DisplayName
thích hiển thị văn bản thay vì tên phương thức bên dưới. Hãy chạy thử nghiệm.
Đối với trường hợp hai số bằng nhau:
@Test
@DisplayName("First number is equal to the second")
public void compare3() {
Basics basicTests = new Basics();
int value = basicTests.compare(2, 2);
Assertions.assertEquals(0, value);
}
Sắp xếp một mảng
Bây giờ, hãy viết bài kiểm tra cho đoạn mã sau để sắp xếp một mảng.
public void sortArray(int[] array) {
int n = array.length;
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (array[j] > array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
Để viết bài kiểm tra cho điều này, chúng ta sẽ làm theo quy trình tương tự: gọi phương thức và truyền một mảng cho nó. Sử dụng assertArrayEquals()
để viết khẳng định của bạn.
@Test
@DisplayName("Array sorted")
public void sortArray() {
Basics basicTests = new Basics();
int[] array = {5, 8, 3, 9, 1, 6};
basicTests.sortArray(array);
Assertions.assertArrayEquals(new int[]{1, 3, 5, 6, 8, 9}, array);
}
Một thử thách dành cho bạn: viết mã đảo ngược chuỗi và viết trường hợp thử nghiệm cho chuỗi đó.
Cách tạo Mocks và Stub để thử nghiệm
Chúng ta đã thấy một số ví dụ cơ bản về kiểm thử đơn vị trong đó bạn thực hiện các xác nhận đơn giản. Tuy nhiên, các chức năng bạn đang kiểm tra có thể chứa các phụ thuộc bên ngoài như các lớp mô hình và cơ sở dữ liệu hoặc kết nối mạng.
Bây giờ, bạn không thể thực hiện các kết nối thực sự trong các bài kiểm tra của mình, vì nó sẽ rất tốn thời gian. Trong những trường hợp như vậy, bạn thử thực hiện như vậy. Hãy xem một vài ví dụ về chế giễu.
chế giễu một lớp học
Hãy tạo một lớp Người dùng với các thuộc tính sau:
public class User {
private String username;
private String password;
private String role;
private List<String> posts;
}
Nhấp vào ⌘ + N
để tạo getters và setters cho các thuộc tính trên.
Hãy tạo một lớp mới Mocking
sử dụng đối tượng trên.
public class Mocking {
User user;
public void setUser(User user) {
this.user = user;
}
}
Lớp này có một phương thức gán các quyền nhất định dựa trên vai trò của người dùng. Nó trả về 1
nếu quyền được gán thành công, ngược lại nó trả về -1
.
public int assignPermission() {
if(user.getRole().equals("admin")) {
String username = user.getUsername();
System.out.println("Assign special permissions for user " + username);
return 1;
} else {
System.out.println("Cannot assign permission");
return -1;
}
}
Đối với mục đích demo, tôi chỉ thêm println()
các câu lệnh. Việc triển khai thực tế có thể liên quan đến việc thiết lập các thuộc tính nhất định.
Trong tệp thử nghiệm, chúng tôi sẽ thêm một @ExtendWith
chú thích ở trên cùng vì chúng tôi đang sử dụng Mockito. Tôi chưa hiển thị các mục nhập ở đây vì IntelliJ thực hiện chúng tự động.
@ExtendWith(MockitoExtension.class)
public class MockingTests {
}
Vậy chúng ta viết test cho method như thế nào? Chúng ta sẽ cần phải chế nhạo User
đối tượng. Bạn có thể làm điều này bằng cách thêm @Mock
chú thích trong khi khai báo đối tượng.
@Mock
User user;
Bạn cũng có thể sử dụng mock()
phương pháp này, vì nó tương tự.
User user = mock(User.class);
Hãy viết phương pháp thử nghiệm.
@Test
@DisplayName("Permission assigned successfully")
public void assignPermissions() {
Mocking mocking = new Mocking();
Assertions.assertEquals(1, mocking.assignPermission());
}
Khi bạn chạy thử nghiệm, nó sẽ ném ra một tệp NullPointerException
.
Điều này là do đối tượng người dùng chưa được khởi tạo. Phương thức bạn gọi không thể sử dụng đối tượng giả. Đối với điều này, bạn sẽ cần gọi setUser
phương thức.
mocking.setUser(user);
Bây giờ, thử nghiệm đưa ra lỗi sau vì đối tượng bị chế nhạo ban đầu chứa đầy giá trị null.
Điều này có nghĩa là bạn cần điền các giá trị thực vào đối tượng giả không? Không, bạn chỉ cần getRole()
phương thức trả về giá trị khác null. Đối với điều đó, chúng tôi sẽ sử dụng phương pháp sơ khai.
when(user.getRole()).thenReturn("admin");
Sử dụng when()...thenReturn()
yêu cầu kiểm tra trả về một giá trị khi một phương thức được gọi. Bạn chỉ nên khai thác các phương thức cho các đối tượng bị chế nhạo.
Chúng tôi sẽ làm tương tự cho getUsername()
phương pháp.
when(user.getUsername()).thenReturn("kunal");
Bây giờ, nếu bạn chạy thử nghiệm, nó sẽ vượt qua.
Ví dụ về phương pháp sơ khai
Trong ví dụ trên, tôi chỉ đơn giản là khai thác các phương thức getter để chứng minh việc khai thác phương thức. Thay vì khai thác getters, bạn có thể đặt vai trò và tên người dùng bằng một hàm tạo được tham số hóa hoặc các phương thức setter nếu chúng khả dụng.
user.setRole("admin");
user.setUsername("kunal");
Nhưng nếu lớp người dùng có một phương thức trả về tất cả các bài đăng có chứa một từ nhất định trong đó thì sao?
public List<String> getAllPostsContainingWord(String word) {
List<String> filteredPosts = new ArrayList<>();
for(String post: posts) {
if(post.contains(word))
filteredPosts.add(post);
}
return filteredPosts;
}
Chúng tôi muốn phương thức này trả về tất cả các bài viết có chứa từ "awesome". Nếu gọi là triển khai thực tế phương pháp này, có thể sẽ mất nhiều thời gian vì số lượng bài đăng có thể rất lớn. Ngoài ra, nếu bạn đang chế nhạo đối tượng Người dùng, thì mảng bài viết sẽ không có giá trị.
Trong trường hợp này, bạn khai thác phương thức và làm cho nó trả về danh sách bạn muốn.
List<String> filteredPosts = new ArrayList<>();
filteredPosts.add("Awesome Day");
filteredPosts.add("This place is awesome");
when(user.getAllPostsContainingWord("awesome")).thenReturn(filteredPosts);
Phương pháp sơ khai trong truy vấn cơ sở dữ liệu
Hãy xem cách kiểm tra các phương pháp liên quan đến việc tạo kết nối cơ sở dữ liệu. Đầu tiên, tạo một lớp ApplicationDao
chứa tất cả các phương thức thực hiện truy vấn cơ sở dữ liệu.
public class ApplicationDao { }
Xác định một phương thức tìm nạp người dùng id
và trả về null
nếu không tìm thấy người dùng.
public User getUserById(String id) {
// Make database query here
}
Tạo một phương thức khác để lưu người dùng vào cơ sở dữ liệu. Phương thức này đưa ra một ngoại lệ nếu đối tượng người dùng mà bạn đang cố lưu là null
.
public void save(User user) throws Exception {
// Make database query here
}
Lớp Mocking của chúng ta sẽ sử dụng các phương thức này để triển khai các chức năng của riêng nó. Chúng tôi sẽ thực hiện một chức năng cập nhật tên của người dùng.
public int updateUsername(String id, String username) throws Exception{
ApplicationDao applicationDao = new ApplicationDao();
User user = applicationDao.getUserById(id);
if(user!=null)
user.setUsername(username);
applicationDao.save(user);
return 1;
}
Việc thực hiện phương pháp là khá đơn giản. Đầu tiên, lấy người dùng theo id
, thay đổi tên người dùng và lưu đối tượng người dùng đã cập nhật. Chúng tôi sẽ viết các trường hợp thử nghiệm cho phương pháp này.
Có hai trường hợp chúng ta cần kiểm tra. Đầu tiên là khi người dùng được cập nhật thành công. Thứ hai là khi cập nhật không thành công, đó là khi một ngoại lệ được đưa ra.
Trước khi viết bài kiểm tra, hãy tạo một bản mô phỏng đối ApplicationDao
tượng vì chúng tôi không muốn thực hiện các kết nối cơ sở dữ liệu thực tế.
@Mock
ApplicationDao applicationDao;
Hãy viết bài kiểm tra đầu tiên của chúng tôi.
@Test
@DisplayName("User updated successfully")
public void updateUsername() throws Exception {
...
}
Tạo một đối tượng người dùng để thử nghiệm.
User user = new User();
user.setUsername("kunal");
Vì chúng ta đang gọi một phương thức bên ngoài, hãy khai thác phương thức để nó trả về User
đối tượng trên.
when(applicationDao.getUserById(Mockito.anyString())).thenReturn(user);
Chuyển Mockito.anyString()
đến phương thức vì chúng tôi muốn sơ khai hoạt động cho bất kỳ tham số chuỗi nào. Bây giờ, hãy thêm một xác nhận để kiểm tra xem phương thức có hoạt động chính xác không.
Assertions.assertEquals(1, mocking.updateUsername("3211", "allan"));
Phương thức này trả về 1 khi cập nhật thành công, vì vậy bài kiểm tra đã vượt qua.
Bây giờ, hãy kiểm tra một tình huống khác trong đó phương thức không thành công và đưa ra một ngoại lệ. Mô phỏng kịch bản này bằng cách làm cho phương thức getUserById()
trả về giá trị rỗng.
lenient().when(applicationDao.getUserById(Mockito.anyString())).thenReturn(null);
Giá trị này sau đó được chuyển đến save()
phương thức, phương thức này sẽ đưa ra một ngoại lệ. Trong khẳng định của chúng tôi, chúng tôi sẽ sử dụng assertThrows()
phương pháp để kiểm tra xem có ngoại lệ nào được đưa ra hay không. Phương thức này lấy loại ngoại lệ và biểu thức lambda làm tham số.
Assertions.assertThrows(Exception.class, () -> {
mocking.updateUsername("3412","allan");
});
Kể từ khi ném ngoại lệ, bài kiểm tra của chúng tôi sẽ vượt qua.
Bạn có thể tìm thấy mã hoàn chỉnh tại đây trên GitHub .
Phần kết luận
Là một nhà phát triển, việc viết các bài kiểm tra đơn vị cho mã của bạn là rất quan trọng. Nó giúp bạn xác định lỗi sớm hơn trong quá trình phát triển.
Trong bài đăng này, tôi bắt đầu bằng cách giới thiệu Kiểm thử đơn vị và giải thích ba khái niệm quan trọng liên quan đến quá trình kiểm thử. Những điều này đã cung cấp cho bạn một số thông tin cơ bản trước khi bắt tay vào viết mã.
Sau đó, tôi đã chỉ cho bạn, kèm theo các ví dụ, cách bạn có thể thử nghiệm các kịch bản khác nhau bằng cách sử dụng các kỹ thuật cơ bản giống nhau trong thử nghiệm. Tôi cũng chỉ ra cách sử dụng các lớp và phương thức giả để thử nghiệm các triển khai phức tạp.