简体   繁体   English

如何使用 JUnit 测试 class 的验证注释?

[英]How to test validation annotations of a class using JUnit?

I need to test the validation annotations but it looks like they do not work.我需要测试 验证注释,但看起来它们不起作用。 I am not sure if the JUnit is also correct.我不确定 JUnit 是否也正确。 Currently, the test will be passed but as you can see the specified email address is wrong.目前,测试将通过,但是您可以看到指定的 email 地址是错误的。

JUnit JUnit

public static void testContactSuccess() {
        Contact contact = new Contact();
        contact.setEmail("Jackyahoo.com");
        contact.setName("Jack");
        System.err.println(contact);
    }

Class to be tested Class待测

public class Contact {

    @NotNull
    @Size(min = 1, max = 10)
    String name;

    @NotNull
    @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\."
            +"[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@"
            +"(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
                 message="{invalid.email}")
    String email;

    @Digits(fraction = 0, integer = 10)
    @Size(min = 10, max = 10)
    String phone;

    getters and setters

}

The other answer saying that "the annotations do not do anything by themselves, you need to use a Validator to process the object" is correct, however, the answer lacks working instructions on how to do it using a Validator instance, which for me was what I really wanted.另一个答案说“注释本身不做任何事情,您需要使用 Validator 来处理对象”是正确的,但是,答案缺少有关如何使用 Validator 实例来执行此操作的工作说明,这对我来说是我真正想要的。

Hibernate-validator is the reference implementation of such a validator. Hibernate-validator 是这种验证器的参考实现。 You can use it quite cleanly like this:您可以像这样非常干净地使用它:

import static org.junit.Assert.assertFalse;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class ContactValidationTest {

    private Validator validator;

    @Before
    public void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    @Test
    public void testContactSuccess() {
        // I'd name the test to something like 
        // invalidEmailShouldFailValidation()

        Contact contact = new Contact();
        contact.setEmail("Jackyahoo.com");
        contact.setName("Jack");
        Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
        assertFalse(violations.isEmpty());
    }
}

This assumes you have validator implementation and junit as dependencies.这假设您有验证器实现和 junit 作为依赖项。

Example of dependencies using Maven pom:使用 Maven pom 的依赖项示例:

<dependency>
    <groupId>org.hibernate</groupId>
    <version>5.2.4.Final</version>
    <artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

A simple way to test validation annotations using javax :使用javax测试验证注释的简单方法:

Declare the Validator at Class level:在类级别声明Validator

private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

Then in your test simply call it on the object you require validation on, with what exception you are validating:然后在您的测试中,只需在您需要验证的object上调用它,您正在验证什么exception

Set<TheViolation<TheClassYouAreValidating> violations = validator.validate(theInstanceOfTheClassYouAreValidating);

Then simply assert the number of expected violations:然后简单地assert预期的违规次数:

assertThat(violations.size()).isEqualTo(1);

You will need to add this to your dependencies ( gradle ):您需要将此添加到您的依赖项( gradle ):

compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'

The annotations do not do anything by themselves, you need to use a Validator to process the object.注释本身不做任何事情,您需要使用 Validator 来处理对象。

Your test needs to run some code like this你的测试需要运行一些这样的代码

    Configuration<?> configuration = Validation
        .byDefaultProvider()
        .providerResolver( new MyResolverStrategy() ) // <== this is where is gets tricky
        .configure();
    ValidatorFactory factory = configuration.buildValidatorFactory();

    Contact contact = new Contact();
    contact.setEmail("Jackyahoo.com");
    contact.setName("Jack");
    factory.getValidator().validate(contact); <== this normally gets run in the background by whatever framework you are using

However, the difficulty you face here are these are all interfaces, you will need implementations to be able to test.但是,您在这里面临的困难是这些都是接口,您需要实现才能进行测试。 You could implement it yourself or find one to use.你可以自己实现它,也可以找一个来使用。

However the question you want to ask yourself is what are you trying to test?但是,您要问自己的问题是您要测试什么? That the hibernate validator works the way it should? or that your regex is correct?或者that your regex is correct?

If this was me I would assume that the Validator works(ie someone else tested that) and focus on the regex.如果这是我,我会假设验证器有效(即其他人测试过)并专注于正则表达式。 Which would involve a bit of reflection这将涉及一些反思

public void emailRegex(String email,boolean validates){

    Field field = Contact.class.getDeclaredField("email");
    javax.validation.constraints.Pattern[] annotations = field.getAnnotationsByType(javax.validation.constraints.Pattern.class);
    assertEquals(email.matches(annotations[0].regexp()),validates);

}

then you can define your testMethods which are actual unit tests然后你可以定义你的 testMethods,它们是实际的单元测试

@Test
public void testInvalidEmail() throws NoSuchFieldException {
    emailRegex("Jackyahoo.com", false);
}

@Test
public void testValidEmail() throws NoSuchFieldException {
    emailRegex("jack@yahoo.com", true);
}

@Test
public void testNoUpperCase() throws NoSuchFieldException {
    emailRegex("Jack@yahoo.com", false);
}

First thanks @Eis for the answer , it helped me.首先感谢@Eis 的回答,它帮助了我。 It's a good way to fail the test, but I wanted a bit more "life-like" behaviour.这是使测试失败的好方法,但我想要更“逼真”的行为。 At runtime an exception would be thrown so I came up with this:在运行时会抛出异常,所以我想出了这个:

/**
 * Simulates the behaviour of bean-validation e.g. @NotNull
 */
private void validateBean(Object bean) throws AssertionError {
    Optional<ConstraintViolation<Object>> violation = validator.validate(bean).stream().findFirst();
    if (violation.isPresent()) {
        throw new ValidationException(violation.get().getMessage());
    }
}

Have an entity with validation:有一个带有验证的实体:

@Data
public class MyEntity {

@NotBlank(message = "Name cannot be empty!")
private String name;

}

In a test you can pass an instance with invalid attributes and expect an exception:在测试中,您可以传递具有无效属性的实例并期望出现异常:

private Validator validator;

@Before
public void setUp() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
}

@Test(expected = ValidationException.class)
public void testValidationWhenNoNameThenThrowException() {
    validateBean(new Entity.setName(""));
}

There are 2 things that you need to check:您需要检查两件事:

The validation rules are configured correctly验证规则配置正确

The validation rules can be checked the way others advise - by creating a validator object and invoking it manually:可以按照其他人建议的方式检查验证规则 - 通过创建验证器对象并手动调用它:

Validator validator = Validation.buildDefaultValidatorFactory().getValidator()
Set violations = validator.validate(contact);
assertFalse(violations.isEmpty());

With this you should check all the possible cases - there could be dozens of them (and in this case there should be dozens of them).有了这个,你应该检查所有可能的情况 - 可能有几十个(在这种情况下应该有几十个)。

The validation is triggered by the frameworks验证由框架触发

In your case you check it with Hibernate, therefore there should be a test that initializes it and triggers some Hibernate operations.在您的情况下,您使用 Hibernate 检查它,因此应该有一个测试来初始化它并触发一些 Hibernate 操作。 Note that for this you need to check only one failing rule for one single field - this will be enough.请注意,为此您只需要检查一个字段的一个失败规则- 这就足够了。 You don't need to check all the rules from again.您无需再次检查所有规则。 Example could be:示例可能是:

@Test(expected = ConstraintViolationException.class)
public void validationIsInvokedBeforeSavingContact() {
  Contact contact = Contact.random();
  contact.setEmail(invalidEmail());
  contactsDao.save(contact)
  session.flush(); // or entityManager.flush();
}

NB: don't forget to trigger flush() .注意:不要忘记触发flush() If you work with UUIDs or sequences as an ID generation strategy, then INSERT is not going to be flushed when you save() - it's going to be postponed until later.如果您使用 UUID 或序列作为 ID 生成策略,那么在您save()时不会刷新 INSERT - 它会被推迟到以后。

This all is a part of how to build a Test Pyramid - you can find more details here .这都是如何构建测试金字塔的一部分 - 您可以在此处找到更多详细信息

Here my way to unit test my objects with fields annotated with some javax.validation.constraints constraints.这是我使用带有一些javax.validation.constraints约束注释的字段对我的对象进行单元测试的方法。
I will give an example with Java 8, JPA entity, Spring Boot and JUnit 5 but the overall idea is the same whatever the context and the frameworks :我将以 Java 8、JPA 实体、Spring Boot 和 JUnit 5 为例,但无论上下文和框架如何,总体思路都是相同的:
We have a nominal scenario where all fields are correctly valued and generally multiple error scenarios where one or more fields are not correctly valued.我们有一个名义上的场景,其中所有字段都被正确赋值,而通常存在多个错误场景,其中一个或多个字段的值不正确。

Testing field validation is not a particularly hard thing.测试现场验证并不是一件特别困难的事情。
But as we have many fields to validate, the tests may become more complex, we can forget some cases, introducing side effects in tests between two cases to validate or simply introduce duplication.但是由于我们有很多字段需要验证,测试可能会变得更加复杂,我们可以忘记一些案例,在两个案例之间的测试中引入副作用来验证或简单地引入重复。
I will give my mind about how to avoid that.我会考虑如何避免这种情况。

In the OP code, we will suppose that the 3 fields have a NotNull constraint.在 OP 代码中,我们假设这 3 个字段具有NotNull约束。 I think that under 3 distinct constraints, the pattern and its value are less visible.我认为在 3 个不同的约束下,模式及其价值不太明显。

I wrote first a unit test for the nominal scenario :我首先为名义场景编写了一个单元测试:

import org.junit.jupiter.api.Test;

@Test
public void persist() throws Exception {       
    Contact contact = createValidContact();

    // action
    contactRepository.save(contact);       
    entityManager.flush();
    entityManager.clear(); 
    // assertion on the id for example
     ...
}

I extract the code to create a valid contact into a method as it will be helpful for no nominal cases :我提取了代码以在方法中创建有效的联系人,因为它对没有名义的情况很有帮助:

private Contact createValidContact(){
   Contact contact = new Contact();
   contact.setEmail("Jackyahoo.com");
   contact.setName("Jack");
   contact.setPhone("33999999");   
   return contact;     
}

Now I write a @parameterizedTest with as fixture source a @MethodSource method :现在我写了一个@parameterizedTest作为夹具源@MethodSource方法:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import javax.validation.ConstraintViolationException;

@ParameterizedTest
@MethodSource("persist_fails_with_constraintViolation_fixture")
void persist_fails_with_constraintViolation(Contact contact ) {
    assertThrows(ConstraintViolationException.class, () -> {
        contactRepository.save(contact);
        entityManager.flush();
    });
}

To compile/run @parameterizedTest , think of adding the required dependency that is not included in the junit-jupiter-api dependency :要编译/运行@parameterizedTest ,请考虑添加未包含在 junit-jupiter-api 依赖项中的所需依赖项:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>${junit-jupiter.version}</version>
    <scope>test</scope>
</dependency>

In the fixture method to create invalid contacts, the idea is simple.在创建无效触点的fixture方法中,思路很简单。 For each case, I create a new valid contact object and I set incorrectly only the field to validate concerned to.对于每种情况,我都创建了一个新的有效联系人对象,并且只错误地设置了要验证的字段。
In this way, I ensure that no side effect between cases are present and that each case provokes itself the expected validation exception as without the field set the valid contact was successful persisted.通过这种方式,我确保案例之间不存在副作用,并且每个案例都会引发预期的验证异常,因为如果没有设置字段,则有效联系人成功保留。

private static Stream<Contact> persist_fails_with_constraintViolation_fixture() {

    Contact contactWithNullName = createValidContact();
    contactWithNullName.setName(null);

    Contact contactWithNullEmail = createValidContact();
    contactWithNullEmail.setEmail(null);

    Contact contactWithNullPhone = createValidContact();
    contactWithNullPhone.setPhone(null);             

    return Stream.of(contactWithNullName, contactWithNullEmail,  contactWithNullPhone);
}

Here is the full test code :这是完整的测试代码:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import javax.validation.ConstraintViolationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit.jupiter.SpringExtension;    

@DataJpaTest
@ExtendWith(SpringExtension.class)
public class ContactRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private ContactRepository contactRepository;

    @BeforeEach
    public void setup() {
        entityManager.clear();
    }

    @Test
    public void persist() throws Exception {       
        Contact contact = createValidContact();

        // action
        contactRepository.save(contact);       
        entityManager.flush();
        entityManager.clear(); 
        // assertion on the id for example
         ...
    }

    @ParameterizedTest
    @MethodSource("persist_fails_with_constraintViolation_fixture")
    void persist_fails_with_constraintViolation(Contact contact ) {
        assertThrows(ConstraintViolationException.class, () -> {
            contactRepository.save(contact);
            entityManager.flush();
        });
    }

    private static Stream<Contact> persist_fails_with_constraintViolation_fixture() {

        Contact contactWithNullName = createValidContact();
        contactWithNullName.setName(null);

        Contact contactWithNullEmail = createValidContact();
        contactWithNullEmail.setEmail(null);

        Contact contactWithNullPhone = createValidContact();
        contactWithNullPhone.setPhone(null);             

        return Stream.of(contactWithNullName, contactWithNullEmail,  contactWithNullPhone);
    }
}

such as:如:

public class Test {
    @Autowired
    private Validator validator;
    public void testContactSuccess() {
        Contact contact = new Contact();
        contact.setEmail("Jackyahoo.com");
        contact.setName("Jack");
        System.err.println(contact);
        Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
        assertTrue(violations.isEmpty());
    }
}

and you also need add bean autowired in your context.xml, such as:并且您还需要在 context.xml 中添加 bean autowired,例如:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
</bean>
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;

public class ValidationTest {

    private Validator validator;

    @Before
    public void init() {

        ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
        this.validator = vf.getValidator();

    }

    @Test
    public void prereqsMet() {
        Workshop validWorkshop = new Workshop(2, 2, true, 3);
        Set<ConstraintViolation<Workshop>> violations = this.validator.validate(validWorkshop);
        assertTrue(violations.isEmpty());
    }  
}

Strictly speaking it is not a unit test, rather an Integration Test.严格来说,它不是单元测试,而是集成测试。 In Unit Test you would like to test the validator logic only, without any dependencies to the SPI.在单元测试中,您只想测试验证器逻辑,而不依赖于 SPI。

https://www.adam-bien.com/roller/abien/entry/unit_integration_testing_the_bean https://www.adam-bien.com/roller/abien/entry/unit_integration_testing_the_bean

If you try using new versions of the validator but land on that thread (like me), you will start getting tons of wired exceptions.如果您尝试使用新版本的验证器但进入该线程(如我),您将开始收到大量有线异常。 So should have in mind that to do test with Hibernate 7+所以应该记住,用Hibernate 7+做测试

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.2.Final</version>
    <scope>test</scope>
</dependency>

should be sure that you are NOT using应该确保你没有使用

<dependency>
    <groupId>javax.validation</groupId>
     <artifactId>validation-api</artifactId>
     <version>2.0.1.Final</version>
</dependency>

but switched to但切换到

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.0.1</version>
</dependency>

and have并且有

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>4.0.2</version>
    <scope>test</scope>
</dependency>

For those with Spring Boot with Spring-Data-JPA you just need to autowire the validator对于使用 Spring 的用户,使用 Spring-Data-JPA 启动,您只需要自动装配验证器

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.validation.Validator;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class ValidatorTest {

    @Autowired
    private Validator validator;

    @Test
    void ensureValidatorIsLoaded() {

        assertThat(validator).isNotNull();
    }

    @Test
    void failValidate() {

        final var violations = validator.validate(new StartRequest());
        assertThat(violations).isNotEmpty();

    }

    @Test
    void passValidate() {

        final var startRequest = StartRequest.builder()
            .contentType("foo/bar")
            .contentMd5Hash("abcdef1234567890abcdef1234567890")
            .category("Pc")
            .contentLength(55)
            .siteId("ca1")
            .desiredExpiration(55)
            .build();
        final var violations = validator.validate(startRequest);
        assertThat(violations).isEmpty();

    }

}

I think validations would work after calling predefined methods which is usually done by the containers mostly not immediately after calling setters of the object.我认为在调用预定义的方法后验证会起作用,这通常由容器完成,而不是在调用对象的 setter 后立即完成。 From the documentation link you shared:从您共享的文档链接:

> By default, the Persistence provider will automatically perform validation on entities with persistent fields or properties annotated with Bean Validation constraints immediately after the PrePersist, PreUpdate, and PreRemove lifecycle events. > 默认情况下,持久化提供程序将在 PrePersist、PreUpdate 和 PreRemove 生命周期事件发生后立即自动对具有持久性字段或属性的实体执行验证,这些实体用 Bean 验证约束进行注释。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM