我真的很喜欢 Java 验证注释来检查方法参数的基本有效性。我的问题是如何在不启动非常慢的Spring应用程序上下文的情况下测试服务bean的这种方法。
我知道验证需要执行一些验证器。通常,当服务 bean 具有 @Validated 时,这是由 Spring Boot 基础设施设置的。有没有办法在不使用创建完整上下文的 @SpringBootTest 的情况下设置单元测试来设置验证?
例如,我有一个实体 Thing 和一个服务 ThingService。
@Entity
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Accessors(chain=true)
public class Thing {
@Id
private Long id;
@NotNull
@Min(value=5)
private Long one;
@NotNull @NotEmpty @NotBlank
@Size(max = 3)
private String two;
public Thing(
Long one,
String two) {
this.one = one;
this.two = two;
}
}
和
@Service
@Validated
public class ThingService {
@Setter
private ThingRepository repo;
@Autowired
public ThingService(ThingRepository repo) {
this.repo = repo;
};
public Thing create(@NotNull Long arg, @NotNull @Valid Thing thing) {
// Use arg to check that thing meets higher level constraints
Thing savedThing = repo.save(thing);
return savedThing;
}
}
我可以使用@SpringBootTest 正确测试,但速度很慢
@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ThingServiceTestOverkill {
@MockBean
private ThingRepository repo;
@Autowired
private ThingService service;
@Test
public void createSuccess() {
when(repo.save(any(Thing.class))).thenAnswer(returnsFirstArg());
Thing thing = service.create(2L, new Thing(5L, "foo"));
verify(repo, times(1)).save(thing);
assertThat(thing.getOne(), is(5L));
assertThat(thing.getTwo(), is ("foo"));
}
@Test
public void createNullUser() {
ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> {
service.create(null, new Thing(5L, "foo"));
});
assertThat(ex.getConstraintViolations(), hasSize(1));
ConstraintViolation<?> cv = ex.getConstraintViolations().stream().findFirst().get();
assertThat(cv.getPropertyPath().toString(), is("create.arg"));
assertThat(cv.getInvalidValue(), is(nullValue()));
assertThat(cv.getMessage(), is("must not be null"));
}
}
如果我尝试使用更传统的直接 Mockito 方法进行测试,则不会进行任何验证。
@ExtendWith(MockitoExtension.class)
public class ThingServiceTestUnderkill {
@Mock
private ThingRepository repo;
// Decided not to use @InjectMocks as it fails silently and can hide errors and bad smells
private ThingService service;
@BeforeEach
public void initializeTest() {
service = new ThingService(repo); // No validation wrapper used.
}
@Test
public void createSuccess() {
when(repo.save(any(Thing.class))).thenAnswer(returnsFirstArg());
Thing thing = service.create(2L, new Thing(5L, "foo"));
verify(repo, times(1)).save(thing);
assertThat(thing.getOne(), is(5L));
assertThat(thing.getTwo(), is ("foo"));
}
@Test
public void createNullUser() {
ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> {
service.create(null, new Thing(5L, "foo"));
});
// NEVER THROWS EXCEPTION
assertThat(ex.getConstraintViolations(), hasSize(1));
ConstraintViolation<?> cv = ex.getConstraintViolations().stream().findFirst().get();
assertThat(cv.getPropertyPath().toString(), is("create.arg"));
assertThat(cv.getInvalidValue(), is(nullValue()));
assertThat(cv.getMessage(), is("must not be null"));
}
}
有没有办法访问 Spring Boot 的验证包装功能?还有其他解决办法吗?
Spring Boot 2.5.2,Java 11。
答案 0 :(得分:0)
经过更多研究,我找到了一个使用 ProxyFactory、MethodBeforeAdvice 和 ExecutableValidator 的不错的解决方案。这是对使用方法验证的类进行单元测试的一种简单快捷的方法。
首先,创建一个被测试类的代理并添加一个建议类(稍后展示)。请参阅@BeforeEach。这是唯一需要的额外工作。
import org.springframework.aop.framework.ProxyFactory;
@ExtendWith(MockitoExtension.class)
public class ThingServiceTestAOP {
@Mock
private ThingRepository repo;
private ThingService service;
@BeforeEach
public void initializeTest() {
ProxyFactory factory = new ProxyFactory(new ThingService(repo));
factory.addAdvice(new ValidationAdvice());
service = (ThingService) factory.getProxy();
}
@Test
public void createSuccess() {
when(repo.save(any(Thing.class))).thenAnswer(returnsFirstArg());
Thing thing = service.create(2L, new Thing(5L, "foo"));
verify(repo, times(1)).save(thing);
assertThat(thing.getOne(), is(5L));
assertThat(thing.getTwo(), is ("foo"));
}
@Test
public void createNullUser() {
ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> {
service.create(null, new Thing(5L, "foo"));
});
assertThat(ex.getConstraintViolations(), hasSize(1));
ConstraintViolation<?> cv = ex.getConstraintViolations().stream().findFirst().get();
assertThat(cv.getPropertyPath().toString(), is("create.userId"));
assertThat(cv.getInvalidValue(), is(nullValue()));
assertThat(cv.getMessage(), is("must not be null"));
}
现在是建议课。静态初始化器块中的代码构造验证器。
注意使用 Spring 的 LocalValidatorFactoryBean 而不是 javax 的 buildDefaultValidatorFactory。前者在确定参数名称方面做得更好。后者只是调用参数“arg0”、“arg1”等。更重要的是,LocalValidatorFactoryBean 获得与 Spring Boot 处理验证时相同的名称,因此您的断言正在测试生产中生成的名称。
before 方法使用标准的 javax 验证代码进行方法验证。
import java.lang.reflect.Method;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.executable.ExecutableValidator;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
public class ValidationAdvice implements MethodBeforeAdvice {
static private ExecutableValidator executableValidator;
static {
// Using the javax validation factory mostly works, but doesn't capture the parameter names
// ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
// LocalValidatorFactoryBean uses DefaultParameterNameDiscoverer which does get the param names
LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean();
factory.afterPropertiesSet();
executableValidator = factory.getValidator().forExecutables();
factory.close();
}
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
Set<ConstraintViolation<Object>> violations = executableValidator.validateParameters(target, method, args);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}