- 数据库:在adm_charge_item表添加SourceBillNo字段 - 后端实体类:更新ChargeItem.java添加SourceBillNo字段 - 前端组件:创建手术计费界面(基于门诊划价界面) - 后端API:扩展PrePrePaymentDto支持手术计费标识 - 后端Service:扩展getChargeItems方法支持手术计费过滤 - 门诊手术安排界面:添加【计费】按钮 注意事项: - 需要手动执行SQL脚本:openhis-server-new/sql/add_source_bill_no_to_adm_charge_item.sql - 术后一站式结算功能待后续开发
546 lines
15 KiB
Markdown
546 lines
15 KiB
Markdown
# Testing - Spring Boot Test
|
|
|
|
## Unit Testing with JUnit 5
|
|
|
|
```java
|
|
@ExtendWith(MockitoExtension.class)
|
|
class UserServiceTest {
|
|
|
|
@Mock
|
|
private UserRepository userRepository;
|
|
|
|
@Mock
|
|
private PasswordEncoder passwordEncoder;
|
|
|
|
@InjectMocks
|
|
private UserService userService;
|
|
|
|
@Test
|
|
@DisplayName("Should create user successfully")
|
|
void shouldCreateUser() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
User user = User.builder()
|
|
.id(1L)
|
|
.email(request.email())
|
|
.username(request.username())
|
|
.build();
|
|
|
|
when(userRepository.existsByEmail(request.email())).thenReturn(false);
|
|
when(passwordEncoder.encode(request.password())).thenReturn("encodedPassword");
|
|
when(userRepository.save(any(User.class))).thenReturn(user);
|
|
|
|
// When
|
|
UserResponse response = userService.create(request);
|
|
|
|
// Then
|
|
assertThat(response).isNotNull();
|
|
assertThat(response.email()).isEqualTo(request.email());
|
|
|
|
verify(userRepository).existsByEmail(request.email());
|
|
verify(passwordEncoder).encode(request.password());
|
|
verify(userRepository).save(any(User.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Should throw exception when email already exists")
|
|
void shouldThrowExceptionWhenEmailExists() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
when(userRepository.existsByEmail(request.email())).thenReturn(true);
|
|
|
|
// When & Then
|
|
assertThatThrownBy(() -> userService.create(request))
|
|
.isInstanceOf(DuplicateResourceException.class)
|
|
.hasMessageContaining("Email already registered");
|
|
|
|
verify(userRepository, never()).save(any(User.class));
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Testing with @SpringBootTest
|
|
|
|
```java
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
@ActiveProfiles("test")
|
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
|
class UserIntegrationTest {
|
|
|
|
@Autowired
|
|
private TestRestTemplate restTemplate;
|
|
|
|
@Autowired
|
|
private UserRepository userRepository;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
@Test
|
|
@Order(1)
|
|
@DisplayName("Should create user via API")
|
|
void shouldCreateUserViaApi() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
// When
|
|
ResponseEntity<UserResponse> response = restTemplate.postForEntity(
|
|
"/api/v1/users",
|
|
request,
|
|
UserResponse.class
|
|
);
|
|
|
|
// Then
|
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
assertThat(response.getBody()).isNotNull();
|
|
assertThat(response.getBody().email()).isEqualTo(request.email());
|
|
assertThat(response.getHeaders().getLocation()).isNotNull();
|
|
}
|
|
|
|
@Test
|
|
@Order(2)
|
|
@DisplayName("Should return validation error for invalid request")
|
|
void shouldReturnValidationError() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"invalid-email",
|
|
"short",
|
|
"u",
|
|
15
|
|
);
|
|
|
|
// When
|
|
ResponseEntity<ValidationErrorResponse> response = restTemplate.postForEntity(
|
|
"/api/v1/users",
|
|
request,
|
|
ValidationErrorResponse.class
|
|
);
|
|
|
|
// Then
|
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
|
assertThat(response.getBody()).isNotNull();
|
|
assertThat(response.getBody().errors()).isNotEmpty();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Web Layer Testing with MockMvc
|
|
|
|
```java
|
|
@WebMvcTest(UserController.class)
|
|
@Import(SecurityConfig.class)
|
|
class UserControllerTest {
|
|
|
|
@Autowired
|
|
private MockMvc mockMvc;
|
|
|
|
@MockBean
|
|
private UserService userService;
|
|
|
|
@Autowired
|
|
private ObjectMapper objectMapper;
|
|
|
|
@Test
|
|
@WithMockUser(roles = "ADMIN")
|
|
@DisplayName("Should get all users")
|
|
void shouldGetAllUsers() throws Exception {
|
|
// Given
|
|
Page<UserResponse> users = new PageImpl<>(List.of(
|
|
new UserResponse(1L, "user1@example.com", "user1", 25, true, null, null),
|
|
new UserResponse(2L, "user2@example.com", "user2", 30, true, null, null)
|
|
));
|
|
|
|
when(userService.findAll(any(Pageable.class))).thenReturn(users);
|
|
|
|
// When & Then
|
|
mockMvc.perform(get("/api/v1/users")
|
|
.contentType(MediaType.APPLICATION_JSON))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.content").isArray())
|
|
.andExpect(jsonPath("$.content.length()").value(2))
|
|
.andExpect(jsonPath("$.content[0].email").value("user1@example.com"))
|
|
.andDo(print());
|
|
}
|
|
|
|
@Test
|
|
@WithMockUser(roles = "ADMIN")
|
|
@DisplayName("Should create user")
|
|
void shouldCreateUser() throws Exception {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
UserResponse response = new UserResponse(
|
|
1L,
|
|
request.email(),
|
|
request.username(),
|
|
request.age(),
|
|
true,
|
|
LocalDateTime.now(),
|
|
LocalDateTime.now()
|
|
);
|
|
|
|
when(userService.create(any(UserCreateRequest.class))).thenReturn(response);
|
|
|
|
// When & Then
|
|
mockMvc.perform(post("/api/v1/users")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(objectMapper.writeValueAsString(request)))
|
|
.andExpect(status().isCreated())
|
|
.andExpect(header().exists("Location"))
|
|
.andExpect(jsonPath("$.email").value(request.email()))
|
|
.andExpect(jsonPath("$.username").value(request.username()))
|
|
.andDo(print());
|
|
}
|
|
|
|
@Test
|
|
@WithMockUser(roles = "USER")
|
|
@DisplayName("Should return 403 for non-admin user")
|
|
void shouldReturn403ForNonAdmin() throws Exception {
|
|
mockMvc.perform(get("/api/v1/users")
|
|
.contentType(MediaType.APPLICATION_JSON))
|
|
.andExpect(status().isForbidden());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Data JPA Testing
|
|
|
|
```java
|
|
@DataJpaTest
|
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
|
@ActiveProfiles("test")
|
|
class UserRepositoryTest {
|
|
|
|
@Autowired
|
|
private UserRepository userRepository;
|
|
|
|
@Autowired
|
|
private TestEntityManager entityManager;
|
|
|
|
@Test
|
|
@DisplayName("Should find user by email")
|
|
void shouldFindUserByEmail() {
|
|
// Given
|
|
User user = User.builder()
|
|
.email("test@example.com")
|
|
.password("password")
|
|
.username("testuser")
|
|
.active(true)
|
|
.build();
|
|
|
|
entityManager.persistAndFlush(user);
|
|
|
|
// When
|
|
Optional<User> found = userRepository.findByEmail("test@example.com");
|
|
|
|
// Then
|
|
assertThat(found).isPresent();
|
|
assertThat(found.get().getEmail()).isEqualTo("test@example.com");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Should check if email exists")
|
|
void shouldCheckIfEmailExists() {
|
|
// Given
|
|
User user = User.builder()
|
|
.email("test@example.com")
|
|
.password("password")
|
|
.username("testuser")
|
|
.active(true)
|
|
.build();
|
|
|
|
entityManager.persistAndFlush(user);
|
|
|
|
// When
|
|
boolean exists = userRepository.existsByEmail("test@example.com");
|
|
|
|
// Then
|
|
assertThat(exists).isTrue();
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Should fetch user with roles")
|
|
void shouldFetchUserWithRoles() {
|
|
// Given
|
|
Role adminRole = Role.builder().name("ADMIN").build();
|
|
entityManager.persist(adminRole);
|
|
|
|
User user = User.builder()
|
|
.email("admin@example.com")
|
|
.password("password")
|
|
.username("admin")
|
|
.active(true)
|
|
.roles(Set.of(adminRole))
|
|
.build();
|
|
|
|
entityManager.persistAndFlush(user);
|
|
entityManager.clear();
|
|
|
|
// When
|
|
Optional<User> found = userRepository.findByEmailWithRoles("admin@example.com");
|
|
|
|
// Then
|
|
assertThat(found).isPresent();
|
|
assertThat(found.get().getRoles()).hasSize(1);
|
|
assertThat(found.get().getRoles()).extracting(Role::getName).contains("ADMIN");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testcontainers for Database
|
|
|
|
```java
|
|
@SpringBootTest
|
|
@Testcontainers
|
|
@ActiveProfiles("test")
|
|
class UserServiceIntegrationTest {
|
|
|
|
@Container
|
|
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
|
|
.withDatabaseName("testdb")
|
|
.withUsername("test")
|
|
.withPassword("test");
|
|
|
|
@DynamicPropertySource
|
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
registry.add("spring.datasource.username", postgres::getUsername);
|
|
registry.add("spring.datasource.password", postgres::getPassword);
|
|
}
|
|
|
|
@Autowired
|
|
private UserService userService;
|
|
|
|
@Autowired
|
|
private UserRepository userRepository;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Should create and find user in real database")
|
|
void shouldCreateAndFindUser() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
// When
|
|
UserResponse created = userService.create(request);
|
|
UserResponse found = userService.findById(created.id());
|
|
|
|
// Then
|
|
assertThat(found).isNotNull();
|
|
assertThat(found.email()).isEqualTo(request.email());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Reactive Endpoints with WebTestClient
|
|
|
|
```java
|
|
@WebFluxTest(UserReactiveController.class)
|
|
class UserReactiveControllerTest {
|
|
|
|
@Autowired
|
|
private WebTestClient webTestClient;
|
|
|
|
@MockBean
|
|
private UserReactiveService userService;
|
|
|
|
@Test
|
|
@DisplayName("Should get user reactively")
|
|
void shouldGetUserReactively() {
|
|
// Given
|
|
UserResponse user = new UserResponse(
|
|
1L,
|
|
"test@example.com",
|
|
"testuser",
|
|
25,
|
|
true,
|
|
LocalDateTime.now(),
|
|
LocalDateTime.now()
|
|
);
|
|
|
|
when(userService.findById(1L)).thenReturn(Mono.just(user));
|
|
|
|
// When & Then
|
|
webTestClient.get()
|
|
.uri("/api/v1/users/{id}", 1L)
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
.exchange()
|
|
.expectStatus().isOk()
|
|
.expectBody(UserResponse.class)
|
|
.value(response -> {
|
|
assertThat(response.id()).isEqualTo(1L);
|
|
assertThat(response.email()).isEqualTo("test@example.com");
|
|
});
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Should create user reactively")
|
|
void shouldCreateUserReactively() {
|
|
// Given
|
|
UserCreateRequest request = new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
|
|
UserResponse response = new UserResponse(
|
|
1L,
|
|
request.email(),
|
|
request.username(),
|
|
request.age(),
|
|
true,
|
|
LocalDateTime.now(),
|
|
LocalDateTime.now()
|
|
);
|
|
|
|
when(userService.create(any(UserCreateRequest.class))).thenReturn(Mono.just(response));
|
|
|
|
// When & Then
|
|
webTestClient.post()
|
|
.uri("/api/v1/users")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.body(Mono.just(request), UserCreateRequest.class)
|
|
.exchange()
|
|
.expectStatus().isCreated()
|
|
.expectHeader().exists("Location")
|
|
.expectBody(UserResponse.class)
|
|
.value(user -> {
|
|
assertThat(user.email()).isEqualTo(request.email());
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Configuration
|
|
|
|
```java
|
|
// application-test.yml
|
|
spring:
|
|
datasource:
|
|
url: jdbc:h2:mem:testdb
|
|
driver-class-name: org.h2.Driver
|
|
jpa:
|
|
hibernate:
|
|
ddl-auto: create-drop
|
|
show-sql: true
|
|
properties:
|
|
hibernate:
|
|
format_sql: true
|
|
security:
|
|
user:
|
|
name: test
|
|
password: test
|
|
|
|
logging:
|
|
level:
|
|
org.hibernate.SQL: DEBUG
|
|
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
|
|
|
// Test Configuration Class
|
|
@TestConfiguration
|
|
public class TestConfig {
|
|
|
|
@Bean
|
|
@Primary
|
|
public PasswordEncoder passwordEncoder() {
|
|
return new BCryptPasswordEncoder(4); // Faster for tests
|
|
}
|
|
|
|
@Bean
|
|
public Clock fixedClock() {
|
|
return Clock.fixed(
|
|
Instant.parse("2024-01-01T00:00:00Z"),
|
|
ZoneId.of("UTC")
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Test Fixtures with @DataJpaTest
|
|
|
|
```java
|
|
@Component
|
|
public class TestDataFactory {
|
|
|
|
public static User createUser(String email, String username) {
|
|
return User.builder()
|
|
.email(email)
|
|
.password("encodedPassword")
|
|
.username(username)
|
|
.active(true)
|
|
.createdAt(LocalDateTime.now())
|
|
.updatedAt(LocalDateTime.now())
|
|
.build();
|
|
}
|
|
|
|
public static UserCreateRequest createUserRequest() {
|
|
return new UserCreateRequest(
|
|
"test@example.com",
|
|
"Password123",
|
|
"testuser",
|
|
25
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Quick Reference
|
|
|
|
| Annotation | Purpose |
|
|
|------------|---------|
|
|
| `@SpringBootTest` | Full application context integration test |
|
|
| `@WebMvcTest` | Test MVC controllers with mocked services |
|
|
| `@WebFluxTest` | Test reactive controllers |
|
|
| `@DataJpaTest` | Test JPA repositories with in-memory database |
|
|
| `@MockBean` | Add mock bean to Spring context |
|
|
| `@WithMockUser` | Mock authenticated user for security tests |
|
|
| `@Testcontainers` | Enable Testcontainers support |
|
|
| `@ActiveProfiles` | Activate specific Spring profiles for test |
|
|
|
|
## Testing Best Practices
|
|
|
|
- Write tests following AAA pattern (Arrange, Act, Assert)
|
|
- Use descriptive test names with @DisplayName
|
|
- Mock external dependencies, use real DB with Testcontainers
|
|
- Achieve 85%+ code coverage
|
|
- Test happy path and edge cases
|
|
- Use @Transactional for test data cleanup
|
|
- Separate unit tests from integration tests
|
|
- Use parameterized tests for multiple scenarios
|
|
- Test security rules and validation
|
|
- Keep tests fast and independent
|