[Thực hành] Kiểm thử ứng dụng quản lý danh sách khách hàng
NỘI DUNG BÀI VIẾT
Mục tiêu
Thực hành sử dụng kiểm thử để phát triển ứng dụng web bằng framework Spring MVC.
Mô tả
Tái sử dụng mã của một dự án có sẵn, bổ sung các kiểm thử cần thiết.
Hướng dẫn
Bước 1: cài đặt dự án và điều tra các chức năng.
Tải dự án tại file đính kèm, thực hiện các cài đặt cần thiết, tạo dữ liệu mẫu và dùng thử các chức năng.
Bước 2: cài đặt unit test framework
Bổ sung các thư viện cần thiết để sử dụng JUnit, những thư viện này sẽ chỉ được compile khi chạy kiểm thử.
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.2.0'testCompile group: 'org.junit.platform', name: 'junit-platform-commons', version: '1.2.0'testCompile group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.2.0'
Bổ sung script để gradle chạy test bằng junit platform vào build.gradle:
test { useJUnitPlatform() testLogging { events "PASSED", "STARTED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR" } afterTest { desc, result -> println "Testing ${desc.name} [${desc.className}]: ${result.resultType}" } reports { html.enabled = true }}
Tạo mới một Gradle Configuration cho IntelliJ, với task là “Test”:

Thử chạy configuration để chắc chắn không có lỗi xảy ra.
Tạo mới một kiểm thử đơn vị, lưu ý đặt file mã tại thư mục test thay vì source.
(Lưu ý: nếu bên trong thư mục test chưa có thư mục con là java thì hãy tạo thêm thư mục java để có thể tạo được các class bên trong.)
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertEquals;class DummyTest { @Test void assertionWorked() { int actual = 1 + 1; int expected = 2; assertEquals(expected, actual); }}
Chạy lại Test configuration và theo dõi kết quả. Thử thay đổi expected thành 3, thực thi lại và theo dõi kết quả. Nếu kết quả là fail (đúng như dự đoán), chứng tỏ JUnit đã được cài đặt thành công. Hãy xóa dummy test và chuyển sang bước sau.
Bước 3: kết nối JUnit và Spring MVC
Mục tiêu của bước này là khiến kiểm thử sau có thể thực thi được:
package cg.wbd.grandemonstration.controller;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.mockito.InjectMocks;import org.mockito.MockitoAnnotations;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;public class CustomerControllerTest { private MockMvc mockMvc; @InjectMocks private CustomerController customerController; @BeforeEach void setUp() { MockitoAnnotations.initMocks(this); mockMvc = MockMvcBuilders.standaloneSetup(customerController).build(); } @Test void customersListPageIsExists() throws Exception { mockMvc .perform(get("/customers")) .andExpect(status().is(200)); }}
Bài kiểm thử trên cố tạo ra một bean controller, giả lập gửi một request tới controller này và xác nhận kết quả hoạt động. Để có thể kiểm thử được, thực hiện những bước cài đặt sau đây.
Bổ sung thư viện spring-tests để có khả năng tích hợp spring mvc với thư viện kiểm thử:
testCompile group: 'org.springframework', name: 'spring-test', version: '4.3.24.RELEASE'testCompile group: 'com.github.sbrannen', name: 'spring-test-junit5', version: '1.2.0'
Bổ sung repo cho thư viện spring-test-junit5:
repositories { mavenCentral() maven { url 'https://jitpack.io' }}
Bổ sung thư viện mokito để có khả năng giả lập hành vi:
testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
Chạy thử kiểm thử. Tới đây kiểm thử đã có thể compile, nhưng khi thực thi, controller đang thiếu thành phần cần thiết để có thể khởi tạo (là bean service). Để đảm bảo sự cô lập khi kiểm thử controller, (các) bean này sẽ không được tạo thật mà được “mock” (giả tạo). Mock bean này bằng cách tạo một class cấu hình test:
package cg.wbd.grandemonstration.controller;import cg.wbd.grandemonstration.service.CustomerService;import cg.wbd.grandemonstration.service.impl.CustomerServiceImplWithSpringData;import org.mockito.Mockito;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@[email protected]("cg.wbd.grandemonstration")public class CustomerControllerTestConfig { @Bean public CustomerService customerService() { return Mockito.mock(CustomerServiceImplWithSpringData.class); }}

Lưu ý bổ sung annotation để các mock service được đưa vào context:
@[email protected](CustomerControllerTestConfig.class)public class CustomerControllerTest {
Đồng thời tiêm đối tượng mock service vào class test:
@Autowiredprivate CustomerService customerService;
Để mock service có thể được tạo ra, sự hiện diện của một entity manager là cần thiết (do service có sử dụng đến repository bean). Có thể mock bean này bằng cách khai báo một mocked datasource và để spring test lo phần còn lại.
Bổ sung thư viện h2 database để có được nhanh nhất một datasource:
testCompile group: 'com.h2database', name: 'h2', version: '1.4.197'
Bổ sung bean datasource vào test config:
@Beanpublic DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .setName("cms") .build();}
Chạy thử kiểm thử, đọc lỗi, và tiếp tục bổ sung mock service còn thiếu:
@Beanpublic ProvinceService provinceService() { return Mockito.mock(ProvinceServiceImplWithSpringData.class);}
—
@Autowiredprivate ProvinceService provinceService;
Chạy thử kiểm thử, nhận thấy rằng các mocked service đã hoạt động, request giả lập đã được gửi nhưng context test đang không phẩn giải đối tượng pageable cho controller – điều mà context chạy thật không gặp phải (nhờ annotation “@EnableSpringDataWebSupport”). Để tái hiện điều này trên test context, bổ sung tham số vào mockMvc builder:
@BeforeEachvoid setUp() { MockitoAnnotations.initMocks(this); mockMvc = MockMvcBuilders .standaloneSetup(customerController) .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver()) .build();}
Chạy kiểm thử, xác nhận rằng kiểm thử đã PASS.
Có thể thay is(200) bằng isOk():
.andExpect(status().isOk())
Bước 4: kiểm thử controller
Bổ sung kiểm thử về view name của màn hình duyệt danh sách khách hàng:
@Testvoid customerBrowseControlling() throws Exception { mockMvc .perform(get("/customers")) .andExpect(status().is(200)) .andExpect(view().name("customers/browse"));}
Chạy test để thấy kiểm thử đã failed, để vượt qua kiểm thử, đổi tên view:
@GetMappingpublic ModelAndView showList(Optional<String> s, Pageable pageInfo) { ModelAndView modelAndView = new ModelAndView("customers/browse");
Bổ sung một kiểm thử khác để kiểm thử cho request update:
@Testvoid customerUpdateSuccessControlling() throws Exception { Customer foo = new Customer(1L, "Foo Bar", "[email protected]", "Nowhere"); when(customerService.save(isA(Customer.class))).thenReturn(foo); mockMvc .perform(post("/customers") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("id", foo.getId().toString()) .param("name", foo.getName()) .param("email", foo.getEmail()) .param("address", foo.getAddress())) .andExpect(status().isFound()) .andExpect(redirectedUrl("/customers"));}
Kiểm thử này giả định trước hành vi của service sẽ là success. Sau đó gửi giả lập một request cập nhật customer. Yêu cầu controller phải trả về redirect tới màn hình duyệt danh sách.
Chạy thử để thấy kiểm thử đã pass.
Bạn có thể tiếp tục bổ sung các kiểm thử controller khác.
Bước 5: tạo kiểm thử service
Tạo bài test cho customer service. Do service sẽ triệu gọi repository nên sẽ cần mock repository.
package cg.wbd.grandemonstration.service;import cg.wbd.grandemonstration.model.Customer;import cg.wbd.grandemonstration.repository.CustomerRepository;import org.junit.jupiter.api.AfterEach;import org.junit.jupiter.api.Test;import org.mockito.Mockito;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageImpl;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.test.context.junit.jupiter.SpringJUnitJupiterConfig;import java.util.ArrayList;import java.util.List;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertNull;import static org.mockito.Mockito.verify;import static org.mockito.Mockito.when;@SpringJUnitJupiterConfig(CustomerServiceTestConfig.class)public class CustomerServiceTest { @Autowired private CustomerService customerService; @Autowired private CustomerRepository customerRepository; @AfterEach private void resetMocks() { Mockito.reset(customerRepository); }}
Provide các đối tượng phụ thuộc:
package cg.wbd.grandemonstration.service;import cg.wbd.grandemonstration.repository.CustomerRepository;import cg.wbd.grandemonstration.service.impl.CustomerServiceImplWithSpringData;import org.mockito.Mockito;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class CustomerServiceTestConfig { @Bean public CustomerRepository customerRepository() { return Mockito.mock(CustomerRepository.class); } @Bean public CustomerService customerService() { return new CustomerServiceImplWithSpringData(); }}
Bước 6: Bổ sung kiểm thử service
@Test
void testFindAll() {
List<Customer> customers = new ArrayList<>();
customers.add(new Customer(1L, "Foo Bar", "[email protected]", "Nowhere"));
Pageable pageInfo = new PageRequest(0, 25);
Page<Customer> customerPage = new PageImpl<Customer>(customers, pageInfo, 1);
when(customerRepository.findAll(pageInfo)).thenReturn(customerPage);
Page<Customer> actual = customerService.findAll(pageInfo);
verify(customerRepository).findAll(pageInfo);
assertEquals(customerPage, actual);
}
Kiểm thử này giả lập hành vi cho repository, sau đó xác nhận rằng khi findAll của service được gọi thì findAll của repository được gọi, cũng như xác nhận rằng kết quả là chính xác.
@Testvoid testFindOneFound() { Customer customer = new Customer(1L, "Foo Bar", "[email protected]", "Nowhere"); when(customerRepository.findOne(1L)).thenReturn(customer); Customer actual = customerService.findOne(1L); verify(customerRepository).findOne(1L); assertEquals(customer, actual);}@Testvoid testFindOneNotFound() { when(customerRepository.findOne(1L)).thenReturn(null); Customer actual = customerService.findOne(1L); verify(customerRepository).findOne(1L); assertNull(actual);}@Testvoid saveCustomer() { Customer customer = new Customer(1L, "Foo Bar", "[email protected]", "Nowhere"); customerService.save(customer); verify(customerRepository).save(customer);}
Tương tự, những kiểm thử này giả lập hành vi cho repository, sau đó xác nhận hành vi của service.
Leave a Reply