【问题标题】: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
        相关资源
        最近更新 更多