feat: 门诊手术中计费功能
- 数据库:在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 - 术后一站式结算功能待后续开发
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user