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

我需要测试的 验证注释,但它们似乎不工作。我不确定 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


}
119767 次浏览

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

The annotations do not do anything by themselves, you need to use a Validator to process the object.

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?

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

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

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:

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

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.

Hibernate-validator is the reference implementation of such a 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.

Example of dependencies using 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>

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. 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(). 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.

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.
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 :
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. I think that under 3 distinct constraints, the pattern and its value are less visible.

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 :

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 :

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

First thanks @Eis for the answer, it helped me. 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(""));
}

A simple way to test validation annotations using javax:

Declare the Validator at Class level:

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:

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

Then simply assert the number of expected violations:

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

You will need to add this to your dependencies (gradle):

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

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.

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+

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