如何将测试方法与其约束验证器(其中一些应该被模拟掉)一起进行单元测试?
How to unit test methods together with its constraint validators (which some should be mocked out)?
我的应用程序有一个由 CDI 应用程序作用域 bean 组成的服务层:
@ApplicationScoped
@Transactional
public class PostService {
@Inject private PostRepository postRepo;
@Inject private UserRepository userRepo;
@Inject private SectionRepository sectionRepo;
@Inject private LoggedInUser loggedInUser;
public PostDto getPost(@PostExists int id){
Post p = postRepo.findById(id);
//create post DTO from p
return post;
}
public void delete(@PostExists int id){
postRepo.remove(postRepo.findById(id));
}
public int newPost(@NotBlank @Max(255) String title,
@Max(2000) String body,
@SectionExists String sectionName){
User user = userRepo.getByName(loggedInUser.getUsername());
Section section = sectionRepo.getByName(sectionName);
Post post = new Post();
post.setTitle(title);
post.setContent(body == null || body.isBlank() ? "" : body);
post.setAuthor(user);
post.setSection(section);
post.setType(TEXT);
return postRepo.insert(post).getId();
}
}
调用方法时,拦截器(在我的例子中来自 Apache BVal BValInterceptor.class
)通过检查注释并相应地验证参数来检查方法契约是否得到遵守。
如您所见,有一些自定义约束,例如 @SectionExists
、@PostExists
可能会影响数据库:
public class SectionExistsValidator implements ConstraintValidator<SectionExists, String> {
@Inject SectionRepository sectionRepo;
@Override
public void initialize(SectionExists constraintAnnotation) {}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return (sectionRepo.getByName(value) != null);
}
}
public class PostExistsValidator implements ConstraintValidator<PostExists, Integer> {
@Inject PostRepository postRepo;
@Override
public void initialize(PostExists constraintAnnotation) {}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return (postRepo.findById(value) != null);
}
}
我想做的是对我的业务方法(getpost
、delete
、newPost
)一起进行单元测试它的验证器。可能命中数据库的验证器应该被模拟(或者它们的依赖性应该被模拟)。
我怎样才能做到这一点?我怎样才能让注入(和模拟注入)在单元测试中为验证器工作?
这是我正在使用的:
- TomEE 8.0.8
- 用于 Bean 验证的 Apache BVal JSR 303/JSR380(包含在 TomEE 中)
- 用于 CDI 的 Apache OpenWebBeans(包含在 TomEE 中)
- JUnit 5
- Mockito
我可以使用 OpenEJB's ApplicationComposer 或 Arquillian 来 运行 一个嵌入式容器。但是,我从未使用过 Arquillian。
最后我选择了this really cool library (cdimock) that does exactly what i needed: put the mocks in a custom CDI scope so that the same mock instances can be injected in other beans inside the test case. Such thing can also be achievable with cdi-unit @Produces @Mock
注释(虽然我没有亲自尝试过,因为它只支持焊接)
这是我的测试class'代码:
@RunWithApplicationComposer(mode = ExtensionMode.PER_EACH)
@ExtendWith({MockitoExtension.class, CdiMocking.class})
@MockitoSettings(strictness = LENIENT)
@Classes(cdi = true,
value={PostService.class},
cdiInterceptors = BValInterceptor.class,
cdiStereotypes = CdiMock.class)
public class PostServiceTest {
@Mock SectionRepository sectionRepository;
@Mock PostRepository postRepository;
@Mock UserRepository userRepository;
@Inject PostService service;
@BeforeEach
void setUp() {}
@AfterEach
void tearDown() {}
@Test
public void noSectionFoundNewPost(){
String sectionName = "idontexist";
when(sectionRepository.getByName(sectionName)).thenReturn(null);
assertThrows(ConstraintViolationException.class,
() -> service.newPost("title", "body", sectionName));
}
}
在代码中,我使用的是 OpenEJB 的 Application Composer,但我可以轻松切换到任何嵌入式 CDI 容器
我的应用程序有一个由 CDI 应用程序作用域 bean 组成的服务层:
@ApplicationScoped
@Transactional
public class PostService {
@Inject private PostRepository postRepo;
@Inject private UserRepository userRepo;
@Inject private SectionRepository sectionRepo;
@Inject private LoggedInUser loggedInUser;
public PostDto getPost(@PostExists int id){
Post p = postRepo.findById(id);
//create post DTO from p
return post;
}
public void delete(@PostExists int id){
postRepo.remove(postRepo.findById(id));
}
public int newPost(@NotBlank @Max(255) String title,
@Max(2000) String body,
@SectionExists String sectionName){
User user = userRepo.getByName(loggedInUser.getUsername());
Section section = sectionRepo.getByName(sectionName);
Post post = new Post();
post.setTitle(title);
post.setContent(body == null || body.isBlank() ? "" : body);
post.setAuthor(user);
post.setSection(section);
post.setType(TEXT);
return postRepo.insert(post).getId();
}
}
调用方法时,拦截器(在我的例子中来自 Apache BVal BValInterceptor.class
)通过检查注释并相应地验证参数来检查方法契约是否得到遵守。
如您所见,有一些自定义约束,例如 @SectionExists
、@PostExists
可能会影响数据库:
public class SectionExistsValidator implements ConstraintValidator<SectionExists, String> {
@Inject SectionRepository sectionRepo;
@Override
public void initialize(SectionExists constraintAnnotation) {}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return (sectionRepo.getByName(value) != null);
}
}
public class PostExistsValidator implements ConstraintValidator<PostExists, Integer> {
@Inject PostRepository postRepo;
@Override
public void initialize(PostExists constraintAnnotation) {}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return (postRepo.findById(value) != null);
}
}
我想做的是对我的业务方法(getpost
、delete
、newPost
)一起进行单元测试它的验证器。可能命中数据库的验证器应该被模拟(或者它们的依赖性应该被模拟)。
我怎样才能做到这一点?我怎样才能让注入(和模拟注入)在单元测试中为验证器工作?
这是我正在使用的:
- TomEE 8.0.8
- 用于 Bean 验证的 Apache BVal JSR 303/JSR380(包含在 TomEE 中)
- 用于 CDI 的 Apache OpenWebBeans(包含在 TomEE 中)
- JUnit 5
- Mockito
我可以使用 OpenEJB's ApplicationComposer 或 Arquillian 来 运行 一个嵌入式容器。但是,我从未使用过 Arquillian。
最后我选择了this really cool library (cdimock) that does exactly what i needed: put the mocks in a custom CDI scope so that the same mock instances can be injected in other beans inside the test case. Such thing can also be achievable with cdi-unit @Produces @Mock
注释(虽然我没有亲自尝试过,因为它只支持焊接)
这是我的测试class'代码:
@RunWithApplicationComposer(mode = ExtensionMode.PER_EACH)
@ExtendWith({MockitoExtension.class, CdiMocking.class})
@MockitoSettings(strictness = LENIENT)
@Classes(cdi = true,
value={PostService.class},
cdiInterceptors = BValInterceptor.class,
cdiStereotypes = CdiMock.class)
public class PostServiceTest {
@Mock SectionRepository sectionRepository;
@Mock PostRepository postRepository;
@Mock UserRepository userRepository;
@Inject PostService service;
@BeforeEach
void setUp() {}
@AfterEach
void tearDown() {}
@Test
public void noSectionFoundNewPost(){
String sectionName = "idontexist";
when(sectionRepository.getByName(sectionName)).thenReturn(null);
assertThrows(ConstraintViolationException.class,
() -> service.newPost("title", "body", sectionName));
}
}
在代码中,我使用的是 OpenEJB 的 Application Composer,但我可以轻松切换到任何嵌入式 CDI 容器