【问题标题】:How to test validation annotations of a class using JUnit?如何使用 JUnit 测试类的验证注释?
【发布时间】:2015-03-16 05:09:39
【问题描述】:

我需要测试validation annotations,但看起来它们不起作用。我不确定 JUnit 是否也正确。目前,测试将通过,但您可以看到指定的电子邮件地址是错误的。

JUnit

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

要测试的类

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

}

【问题讨论】:

标签: java junit


【解决方案1】:

other answer 说“注释本身不会做任何事情,您需要使用 Validator 来处理对象”是正确的,但是,答案缺乏关于如何使用 Validator 实例来执行此操作的工作说明,这对我来说是我真正想要的。

Hibernate-validator 是此类验证器的参考实现。您可以像这样非常干净地使用它:

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());
    }
}

这假设您有验证器实现和 junit 作为依赖项。

使用 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>

【讨论】:

  • 如果使用Spring,则可以注入Validator,例如:@Autowired private Validator validator;
  • @StanislavKarakhanov 是的,就像在 this answer 中扩展的一样。但是,如果在此测试中尚未使用 Spring,则不需要 Spring 依赖项。
  • 使用 @Autowired 的验证器,您需要启动 Spring 上下文。使用 ValidatorFactory 不要。
  • @HomayounBehzadian 如果未检查嵌套字段,则说明您对它们进行了错误的注释。同样的测试也适用于嵌套。
  • 每个字段都需要@Valid注解
【解决方案2】:

使用javax 测试验证注释的简单方法:

在类级别声明Validator

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

然后在您的测试中,只需在您需要验证的 object 上调用它,使用您正在验证的 exception

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

然后简单地assert 预期违规次数:

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

您需要将此添加到您的依赖项 (gradle):

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

【讨论】:

  • 难道你不需要在类路径中也有一个验证器实现,而不仅仅是 api?
【解决方案3】:

注解本身不做任何事情,您需要使用Validator来处理对象。

你的测试需要运行一些这样的代码

    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

但是,您在这里面临的困难是这些都是接口,您需要实现才能进行测试。你可以自己实现它或找到一个来使用。

但是,您想问自己的问题是您要测试什么? That the hibernate validator works the way it should?that your regex is correct?

如果这是我,我会假设验证器有效(即其他人测试过)并专注于正则表达式。这将涉及一些反思

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);

}

然后你可以定义你的 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);
}

【讨论】:

  • 正则表达式应该是正确的,因为我在 Oracle 网站上找到了它,但您的评论让我想到它在代码运行时是否真的有效。
  • @Jack 正则表达式似乎不允许使用大写字符
【解决方案4】:

首先感谢@Eis for the answer,它帮助了我。这是考试失败的好方法,但我想要更多“栩栩如生”的行为。在运行时会抛出异常,所以我想出了这个:

/**
 * 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());
    }
}

有一个带有验证的实体:

@Data
public class MyEntity {

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

}

在测试中,您可以传递具有无效属性的实例并期待异常:

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(""));
}

【讨论】:

    【解决方案5】:

    这是我使用带有javax.validation.constraints 约束注释的字段对对象进行单元测试的方法。
    我将举一个 Java 8、JPA 实体、Spring Boot 和 JUnit 5 的示例,但无论上下文和框架如何,总体思路都是相同的:
    我们有一个名义上的场景,其中所有字段都被正确赋值并且通常多个错误场景,其中一个或多个字段的值不正确。

    测试字段验证并不是一件特别难的事情。
    但是由于我们要验证的字段很多,测试可能会变得更加复杂,我们可以忘记一些案例,在两个案例之间的测试中引入副作用来验证或简单地引入重复。
    我会考虑如何避免这种情况。

    在 OP 代码中,我们将假设 3 个字段具有NotNull 约束。我认为在 3 个不同的约束条件下,模式及其价值不太明显。

    我首先为名义场景编写了一个单元测试:

    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
         ...
    }
    

    我将创建有效联系人的代码提取到一个方法中,因为这将有助于没有名义上的情况:

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

    现在我写了一个@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();
        });
    }
    

    要编译/运行@parameterizedTest,请考虑添加未包含在junit-jupiter-api依赖项中的必需依赖项:

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

    在fixture方法中创建无效联系人,思路很简单。对于每种情况,我都会创建一个新的有效联系人对象,并且我只错误地设置了要验证的字段。
    通过这种方式,我确保案例之间不存在任何副作用,并且每个案例都会引发预期的验证异常,因为没有设置字段,有效的联系人会成功持久化。

    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);
    }
    

    这是完整的测试代码:

    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);
        }
    }
    

    【讨论】:

    • 我认为这是可行的,因为 NotNull 注释对数据库本身有影响。在数据库设计中,数据库字段将不为空。在我的情况下,我的实体在一个不会导致 DataJpaTest 中的 ConstraintViolationException 的字段上有一个 javax.validation.constraints.Pattern 注释。
    【解决方案6】:
    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());
        }  
    }
    

    严格来说,它不是单元测试,而是集成测试。在单元测试中,您只想测试验证器逻辑,而不依赖于 SPI。

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

    【讨论】:

      【解决方案7】:

      您需要检查两件事:

      验证规则配置正确

      可以按照其他人建议的方式检查验证规则 - 通过创建验证器对象并手动调用它:

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

      有了这个,您应该检查所有可能的情况 - 可能有几十个(在这种情况下,应该有几十个)。

      验证由框架触发

      在您的情况下,您使用 Hibernate 检查它,因此应该有一个测试来初始化它并触发一些 Hibernate 操作。请注意,为此您只需要检查一个字段的一个失败规则 - 这已经足够了。您无需再次检查所有规则。示例可能是:

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

      注意:不要忘记触发flush()。如果您使用 UUID 或序列作为 ID 生成策略,那么当您 save() 时,INSERT 不会被刷新 - 它将被推迟到以后。

      这都是如何构建测试金字塔的一部分 - 你可以找到more details here

      【讨论】:

      • 感谢您提供 UUID 信息,这对我来说是新的,节省了很多时间!
      • 先生,谢谢你的回答对我有帮助,你能看看我的问题stackoverflow.com/q/64572296/9398992
      【解决方案8】:

      如:

      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());
          }
      }
      

      你还需要在你的 context.xml 中添加自动装配的 bean,例如:

      <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
      </bean>
      

      【讨论】:

      • 这取决于 Spring - OP 没有这样的声明,我不建议为此添加 Spring。
      【解决方案9】:

      如果您尝试使用新版本的验证器但进入该线程(如我),您将开始收到大量有线异常。所以应该记住要使用 Hibernate 7+

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

      应确保您使用

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

      但切换到

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

      并且拥有

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

      【讨论】:

        【解决方案10】:

        我认为验证会在调用预定义方法后起作用,这通常由容器完成,而不是在调用对象的 setter 后立即进行。从您分享的文档链接:

        > 默认情况下,Persistence 提供程序将在 PrePersist、PreUpdate 和 PreRemove 生命周期事件之后立即自动对具有持久字段或属性的实体执行 Bean Validation 约束注释。

        【讨论】:

        • 我读到了,谢谢,不过应该有办法测试它们。为什么即使输入错误的值也能通过测试?
        猜你喜欢
        • 1970-01-01
        • 2018-09-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-04
        • 2021-12-24
        • 2011-12-03
        • 2010-11-27
        相关资源
        最近更新 更多