JavaRush /Blog Java /Random-VI /Kiểm thử đơn vị Java: kỹ thuật, khái niệm, thực hành

Kiểm thử đơn vị Java: kỹ thuật, khái niệm, thực hành

Xuất bản trong nhóm
Ngày nay, bạn khó có thể tìm thấy một ứng dụng không có các bài kiểm tra, vì vậy chủ đề này sẽ phù hợp hơn bao giờ hết đối với các nhà phát triển mới vào nghề: bạn không thể đi đến đâu nếu không có bài kiểm tra. Là một quảng cáo, tôi khuyên bạn nên xem các bài viết trước đây của tôi. Một số trong số chúng bao gồm các bài kiểm tra (và thậm chí các bài viết sẽ rất hữu ích):
  1. Kiểm tra tích hợp cơ sở dữ liệu bằng MariaDB để thay thế MySql
  2. Triển khai ứng dụng đa ngôn ngữ
  3. Lưu tệp vào ứng dụng và dữ liệu về chúng vào cơ sở dữ liệu
Về nguyên tắc, hãy xem xét những loại thử nghiệm nào được sử dụng và sau đó chúng tôi sẽ nghiên cứu chi tiết mọi thứ bạn cần biết về thử nghiệm đơn vị.

Các loại thử nghiệm

Kiểm tra là gì? Như Wiki đã nói: “ Kiểm tra hoặc kiểm tra là một cách nghiên cứu các quy trình cơ bản của hệ thống bằng cách đặt hệ thống vào các tình huống khác nhau và theo dõi những thay đổi có thể quan sát được trong đó”. Nói cách khác, đây là cuộc kiểm tra hoạt động chính xác của hệ thống của chúng tôi trong một số tình huống nhất định. Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 2Nào, hãy xem có những loại thử nghiệm nào:
  1. Kiểm thử đơn vị là các kiểm thử có nhiệm vụ kiểm thử từng mô-đun của hệ thống riêng lẻ. Điều mong muốn là đây là những phần có thể phân chia tối thiểu của hệ thống, ví dụ như các mô-đun.

  2. Kiểm thử hệ thống là kiểm thử cấp cao để kiểm tra hoạt động của một phần lớn hơn của ứng dụng hoặc toàn bộ hệ thống.

  3. Kiểm thử hồi quy là kiểm thử được sử dụng để kiểm tra xem các tính năng mới hoặc sửa lỗi có ảnh hưởng đến chức năng hiện có của ứng dụng hay không và liệu các lỗi cũ có xuất hiện trở lại hay không.

  4. Kiểm tra chức năng là kiểm tra sự tuân thủ của một phần ứng dụng với các yêu cầu được nêu trong thông số kỹ thuật, câu chuyện của người dùng, v.v.

    Các loại thử nghiệm chức năng:

    • kiểm tra “hộp trắng” về sự tuân thủ của một phần ứng dụng với các yêu cầu cùng với kiến ​​thức về triển khai nội bộ của hệ thống;
    • Kiểm tra “hộp đen” về sự tuân thủ của một phần ứng dụng với các yêu cầu mà không cần biết về việc triển khai nội bộ của hệ thống.
  5. Kiểm tra hiệu suất là một loại kiểm tra được viết để xác định tốc độ mà hệ thống hoặc một phần của hệ thống chạy dưới một tải nhất định.
  6. Kiểm tra tải - các kiểm tra được thiết kế để kiểm tra tính ổn định của hệ thống dưới tải tiêu chuẩn và tìm ra mức cao nhất có thể mà ứng dụng hoạt động chính xác.
  7. Kiểm thử căng thẳng là một loại kiểm thử được thiết kế để kiểm tra chức năng của ứng dụng dưới các tải không chuẩn và để xác định mức cao nhất có thể mà hệ thống sẽ không gặp sự cố.
  8. Kiểm tra bảo mật - kiểm tra được sử dụng để kiểm tra tính bảo mật của hệ thống (trước các cuộc tấn công của tin tặc, vi rút, truy cập trái phép vào dữ liệu bí mật và các thú vui khác trong cuộc sống).
  9. Thử nghiệm bản địa hóa là thử nghiệm bản địa hóa cho một ứng dụng.
  10. Kiểm thử khả năng sử dụng là loại kiểm thử nhằm kiểm tra khả năng sử dụng, tính dễ hiểu, tính hấp dẫn và khả năng học hỏi của người dùng.
  11. Tất cả điều này nghe có vẻ tốt, nhưng nó hoạt động như thế nào trong thực tế? Thật đơn giản: Kim tự tháp thử nghiệm của Mike Cohn được sử dụng: Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 4Đây là phiên bản đơn giản hóa của kim tự tháp: bây giờ nó được chia thành các phần nhỏ hơn. Nhưng hôm nay chúng ta sẽ không đi chệch hướng và xem xét lựa chọn đơn giản nhất.
    1. Đơn vị - các bài kiểm tra đơn vị được sử dụng trong các lớp khác nhau của ứng dụng, kiểm tra logic chia nhỏ nhất của ứng dụng: ví dụ: một lớp, nhưng thường là một phương thức. Các thử nghiệm này thường cố gắng tách biệt càng nhiều càng tốt khỏi logic bên ngoài, nghĩa là tạo ra ảo tưởng rằng phần còn lại của ứng dụng đang hoạt động ở chế độ tiêu chuẩn.

      Phải luôn có nhiều thử nghiệm kiểu này (nhiều hơn các loại khác), vì chúng thử nghiệm từng phần nhỏ và rất nhẹ, không tiêu tốn nhiều tài nguyên (ý tôi là tài nguyên RAM và thời gian).

    2. Tích hợp - thử nghiệm tích hợp. Nó kiểm tra các phần lớn hơn của hệ thống, nghĩa là nó là sự kết hợp của một số phần logic (một số phương thức hoặc lớp) hoặc tính chính xác khi làm việc với một thành phần bên ngoài. Thường có ít bài kiểm tra kiểu này hơn bài kiểm tra Đơn vị vì chúng nặng hơn.

      Là một ví dụ về kiểm tra tích hợp, bạn có thể xem xét việc kết nối với cơ sở dữ liệu và kiểm tra việc thực thi chính xác các phương thức làm việc với cơ sở dữ liệu đó .

    3. UI - kiểm tra hoạt động của giao diện người dùng. Chúng ảnh hưởng đến logic ở mọi cấp độ của ứng dụng, đó là lý do tại sao chúng còn được gọi là end-to-end. Theo quy định, có ít hơn nhiều trong số chúng, vì chúng nặng nhất và phải kiểm tra các đường dẫn cần thiết (đã sử dụng) nhất.

      Trong hình trên, chúng ta thấy tỷ lệ diện tích của các phần khác nhau của tam giác: tỷ lệ gần như tương tự được duy trì trong số lượng các thử nghiệm này trong công việc thực tế.

      Hôm nay chúng ta sẽ xem xét kỹ hơn các bài kiểm tra được sử dụng nhiều nhất - bài kiểm tra đơn vị, vì tất cả các nhà phát triển Java tự trọng đều có thể sử dụng chúng ở mức cơ bản.

    Các khái niệm chính của kiểm thử đơn vị

    Phạm vi kiểm thử (Code Coverage) là một trong những đánh giá chính về chất lượng kiểm thử ứng dụng. Đây là tỷ lệ phần trăm mã được kiểm tra (0-100%). Trong thực tế, nhiều người theo đuổi tỷ lệ phần trăm này, điều mà tôi không đồng ý, vì họ bắt đầu thêm các bài kiểm tra vào những nơi không cần thiết. Ví dụ: dịch vụ của chúng tôi có các hoạt động CRUD (tạo/lấy/cập nhật/xóa) tiêu chuẩn mà không cần logic bổ sung. Các phương thức này hoàn toàn là trung gian ủy thác công việc cho lớp làm việc với kho lưu trữ. Trong tình huống này, chúng tôi không có gì để kiểm tra: có lẽ phương pháp này có gọi là phương pháp của Đạo hay không, nhưng điều này không nghiêm trọng. Để đánh giá phạm vi kiểm tra, các công cụ bổ sung thường được sử dụng: JaCoCo, Cobertura, Clover, Emma, ​​​​v.v. Để nghiên cứu chi tiết hơn về vấn đề này, hãy giữ một vài bài viết phù hợp: TDD (Phát triển dựa trên thử nghiệm) - phát triển dựa trên thử nghiệm. Trong phương pháp này, trước hết, một bài kiểm tra được viết sẽ kiểm tra một mã cụ thể. Hóa ra đây là thử nghiệm hộp đen: chúng tôi biết đầu vào là gì và chúng tôi biết điều gì sẽ xảy ra ở đầu ra. Điều này tránh trùng lặp mã. Phát triển dựa trên thử nghiệm bắt đầu bằng việc thiết kế và phát triển các thử nghiệm cho từng chức năng nhỏ của ứng dụng. Theo cách tiếp cận TDD, trước tiên, một thử nghiệm được phát triển để xác định và xác minh xem mã sẽ làm gì. Mục tiêu chính của TDD là làm cho mã rõ ràng hơn, đơn giản hơn và không có lỗi. Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 6Cách tiếp cận bao gồm các thành phần sau:
    1. Chúng tôi đang viết bài kiểm tra của chúng tôi.
    2. Chúng tôi chạy thử nghiệm, cho dù nó có vượt qua hay không (chúng tôi thấy mọi thứ đều có màu đỏ - đừng lo lắng: nó phải như vậy).
    3. Chúng tôi thêm mã phải đáp ứng thử nghiệm này (chạy thử nghiệm).
    4. Chúng tôi cấu trúc lại mã.
    Dựa trên thực tế rằng các bài kiểm tra đơn vị là những phần tử nhỏ nhất trong kim tự tháp tự động hóa kiểm thử, TDD dựa trên chúng. Với sự trợ giúp của các bài kiểm tra đơn vị, chúng ta có thể kiểm tra logic nghiệp vụ của bất kỳ lớp nào. BDD (Phát triển theo định hướng hành vi) - phát triển thông qua hành vi. Cách tiếp cận này dựa trên TDD. Cụ thể hơn, nó sử dụng các ví dụ được viết bằng ngôn ngữ rõ ràng (thường bằng tiếng Anh) để minh họa hành vi của hệ thống cho mọi người tham gia vào quá trình phát triển. Chúng tôi sẽ không đi sâu hơn vào thuật ngữ này vì nó chủ yếu ảnh hưởng đến những người thử nghiệm và nhà phân tích kinh doanh. Test Case - một tập lệnh mô tả các bước, điều kiện cụ thể và các tham số cần thiết để xác minh việc triển khai mã đang được thử nghiệm. Lịch thi đấu là trạng thái của môi trường thử nghiệm cần thiết để thực hiện thành công phương pháp được thử nghiệm. Đây là một tập hợp các đối tượng được xác định trước và hành vi của chúng trong các điều kiện được sử dụng.

    Giai đoạn thử nghiệm

    Bài kiểm tra bao gồm ba giai đoạn:
    1. Chỉ định dữ liệu cần kiểm tra (đồ đạc).
    2. Sử dụng mã đang thử nghiệm (gọi phương thức đang thử nghiệm).
    3. Kiểm tra kết quả và so sánh chúng với kết quả mong đợi.
    Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 7Để đảm bảo tính mô đun thử nghiệm, bạn cần tách biệt khỏi các lớp khác của ứng dụng. Điều này có thể được thực hiện bằng cách sử dụng stub, mock và spy. Mô phỏng là các đối tượng có thể tùy chỉnh (ví dụ: cụ thể cho từng thử nghiệm) và cho phép bạn đặt kỳ vọng cho các lệnh gọi phương thức dưới dạng phản hồi mà chúng tôi dự định nhận. Kiểm tra kỳ vọng được thực hiện thông qua các cuộc gọi đến đối tượng Mock. Sơ khai - cung cấp phản hồi có dây cứng cho các cuộc gọi trong quá trình thử nghiệm. Họ cũng có thể lưu trữ thông tin về cuộc gọi (ví dụ: thông số hoặc số lượng các cuộc gọi này). Đôi khi chúng được gọi bằng thuật ngữ riêng - gián điệp ( Spy ). Đôi khi các thuật ngữ sơ khaimô phỏng này bị nhầm lẫn: sự khác biệt là sơ khai không kiểm tra bất cứ điều gì mà chỉ mô phỏng một trạng thái nhất định. Một mock là một đối tượng có sự mong đợi. Ví dụ: một phương thức lớp nhất định phải được gọi một số lần nhất định. Nói cách khác, bài kiểm tra của bạn sẽ không bao giờ bị hỏng do sơ khai, nhưng nó có thể bị hỏng do mô phỏng.

    Môi trường thử nghiệm

    Vậy bây giờ chúng ta hãy bắt tay vào công việc. Có một số môi trường thử nghiệm (khung) có sẵn cho Java. Phổ biến nhất trong số đó là JUnit và TestNG. Để xem xét, chúng tôi sử dụng: Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 8Bài kiểm tra JUnit là một phương thức có trong một lớp chỉ được sử dụng để kiểm tra. Một lớp thường được đặt tên giống với lớp mà nó đang kiểm tra với +Test ở cuối. Ví dụ: CarService→ CarServiceTest. Hệ thống xây dựng Maven tự động bao gồm các lớp như vậy trong khu vực thử nghiệm. Trên thực tế, lớp này được gọi là lớp kiểm tra. Chúng ta hãy điểm qua các chú thích cơ bản một chút: @Test - định nghĩa phương thức này là một phương thức thử nghiệm (trên thực tế, phương thức được đánh dấu bằng chú thích này là một thử nghiệm đơn vị). @Before - đánh dấu phương thức sẽ được thực thi trước mỗi lần kiểm tra. Ví dụ: điền dữ liệu kiểm tra lớp, đọc dữ liệu đầu vào, v.v. @After - được đặt phía trên phương thức sẽ được gọi sau mỗi lần kiểm tra (làm sạch dữ liệu, khôi phục giá trị mặc định). @BeforeClass - được đặt phía trên phương thức - tương tự như @Before. Nhưng phương thức này chỉ được gọi một lần trước tất cả các bài kiểm tra cho một lớp nhất định và do đó phải ở dạng tĩnh. Nó được sử dụng để thực hiện các hoạt động nặng hơn, chẳng hạn như nâng cơ sở dữ liệu thử nghiệm. @AfterClass ngược lại với @BeforeClass: được thực thi một lần cho một lớp nhất định, nhưng được thực thi sau tất cả các lần kiểm tra. Ví dụ: được sử dụng để dọn sạch các tài nguyên liên tục hoặc ngắt kết nối khỏi cơ sở dữ liệu. @Ignore - lưu ý rằng phương pháp bên dưới bị tắt và sẽ bị bỏ qua khi chạy thử nghiệm tổng thể. Nó được sử dụng trong các trường hợp khác nhau, ví dụ, nếu phương pháp cơ bản đã bị thay đổi và không có thời gian để thực hiện lại quá trình kiểm tra nó. Trong những trường hợp như vậy, bạn cũng nên thêm mô tả - @Ignore("Some description"). @Test (expected = Exception.class) - được sử dụng cho các thử nghiệm âm tính. Đây là các thử nghiệm kiểm tra cách một phương thức hoạt động trong trường hợp có lỗi, nghĩa là thử nghiệm mong muốn phương thức đó đưa ra một số ngoại lệ. Phương thức như vậy được biểu thị bằng chú thích @Test nhưng có lỗi cần phát hiện. @Test(timeout=100) - kiểm tra xem phương thức có thực thi trong không quá 100 mili giây hay không. @Mock - một lớp được sử dụng trên một trường để đặt một đối tượng nhất định làm mô hình (đây không phải từ thư viện Junit, mà từ Mockito) và nếu cần, chúng tôi sẽ đặt hành vi của mô hình trong một tình huống cụ thể , trực tiếp trong phương pháp thử nghiệm. @RunWith(MockitoJUnitRunner.class) - phương thức được đặt phía trên lớp. Đây là nút để chạy thử nghiệm trong đó. Người chạy có thể khác nhau: ví dụ: có những thứ sau: MockitoJUnitRunner, JUnitPlatform, SpringRunner, v.v.). Trong JUnit 5, chú thích @RunWith đã được thay thế bằng chú thích @ExtendWith mạnh mẽ hơn. Chúng ta hãy xem xét một số phương pháp để so sánh kết quả:
    • assertEquals(Object expecteds, Object actuals)- kiểm tra xem các đối tượng được truyền có bằng nhau hay không.
    • assertTrue(boolean flag)— kiểm tra xem giá trị được truyền có trả về true hay không.
    • assertFalse(boolean flag)— kiểm tra xem giá trị đã truyền có trả về sai hay không.
    • assertNull(Object object)– kiểm tra xem đối tượng có rỗng không.
    • assertSame(Object firstObject, Object secondObject)— kiểm tra xem các giá trị được truyền có tham chiếu đến cùng một đối tượng hay không.
    • assertThat(T t, Matcher<T> matcher)- kiểm tra xem t có thỏa mãn điều kiện được chỉ định trong trình so khớp hay không.
    Ngoài ra còn có một dạng so sánh hữu ích từ khẳng địnhj - assertThat(firstObject).isEqualTo(secondObject) Ở đây tôi đã nói về các phương pháp cơ bản, vì phần còn lại là các biến thể khác nhau của các phương pháp trên.

    Thực hành kiểm tra

    Bây giờ chúng ta hãy xem tài liệu trên bằng một ví dụ cụ thể. Chúng tôi sẽ thử nghiệm phương pháp cho dịch vụ - cập nhật. Chúng tôi sẽ không xem xét lớp dao vì đây là lớp mặc định của chúng tôi. Hãy thêm phần khởi đầu cho các bài kiểm tra:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Vì vậy, lớp dịch vụ:
    @Service
    @RequiredArgsConstructor
    public class RobotServiceImpl implements RobotService {
       private final RobotDAO robotDAO;
    
       @Override
       public Robot update(Long id, Robot robot) {
           Robot found = robotDAO.findById(id);
           return robotDAO.update(Robot.builder()
                   .id(id)
                   .name(robot.getName() != null ? robot.getName() : found.getName())
                   .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
                   .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
                   .build());
       }
    }
    8 - kéo đối tượng đã cập nhật từ cơ sở dữ liệu 9-14 - tạo đối tượng thông qua trình tạo, nếu đối tượng đến có một trường - đặt nó, nếu không - để lại những gì có trong cơ sở dữ liệu Và xem thử nghiệm của chúng tôi:
    @RunWith(MockitoJUnitRunner.class)
    public class RobotServiceImplTest {
       @Mock
       private RobotDAO robotDAO;
    
       private RobotServiceImpl robotService;
    
       private static Robot testRobot;
    
       @BeforeClass
       public static void prepareTestData() {
           testRobot = Robot
                   .builder()
                   .id(123L)
                   .name("testRobotMolly")
                   .cpu("Intel Core i7-9700K")
                   .producer("China")
                   .build();
       }
    
       @Before
       public void init() {
           robotService = new RobotServiceImpl(robotDAO);
       }
    1 — Á hậu 4 của chúng tôi — cách ly dịch vụ khỏi lớp dao bằng cách thay thế một mô hình 11 — đặt thực thể kiểm tra cho lớp (đối tượng mà chúng tôi sẽ sử dụng làm chuột đồng thử nghiệm) 22 — đặt đối tượng dịch vụ mà chúng tôi sẽ kiểm tra
    @Test
    public void updateTest() {
       when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
       when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
       Robot robotForUpdate = Robot
               .builder()
               .name("Vally")
               .cpu("AMD Ryzen 7 2700X")
               .build();
    
       Robot resultRobot = robotService.update(123L, robotForUpdate);
    
       assertNotNull(resultRobot);
       assertSame(resultRobot.getId(),testRobot.getId());
       assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
       assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
       assertEquals(resultRobot.getProducer(),testRobot.getProducer());
    }
    Ở đây chúng ta thấy sự phân chia rõ ràng của bài kiểm tra thành ba phần: 3-9 - thiết lập đồ đạc 11 - thực hiện phần đã kiểm tra 13-17 - kiểm tra kết quả Chi tiết hơn: 3-4 - thiết lập hành vi cho moka dao 5 - thiết lập phiên bản mà chúng tôi sẽ cập nhật dựa trên tiêu chuẩn 11 của chúng tôi - sử dụng phương thức và lấy phiên bản kết quả 13 - kiểm tra xem nó có bằng 0 14 - kiểm tra ID kết quả và các đối số phương thức đã chỉ định 15 - kiểm tra xem tên đã được cập nhật chưa 16 - nhìn vào kết quả của cpu 17 - vì chúng tôi không đặt cái này trong trường phiên bản cập nhật nên nó sẽ giữ nguyên, hãy kiểm tra nó. Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 9Hãy khởi động: Tất cả về Kiểm thử đơn vị: kỹ thuật, khái niệm, thực hành - 10Bài kiểm tra có màu xanh, bạn có thể thở ra)) Vì vậy, hãy tóm tắt: kiểm tra cải thiện chất lượng mã và làm cho quá trình phát triển linh hoạt và đáng tin cậy hơn. Hãy tưởng tượng chúng ta sẽ phải tốn bao nhiêu công sức khi thiết kế lại phần mềm với hàng trăm file lớp. Khi chúng tôi đã viết bài kiểm tra đơn vị cho tất cả các lớp này, chúng tôi có thể tự tin tái cấu trúc. Và quan trọng nhất là nó giúp chúng ta dễ dàng tìm ra lỗi trong quá trình phát triển. Các bạn ơi, hôm nay đối với tôi chỉ có vậy thôi: đổ lượt thích, viết bình luận))) Tất cả về Unit testing: phương pháp, khái niệm, thực hành - 11
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION