5

I am creating the tests for two classes that share a list of data. When I use EqualsVerifier I get an error because it is asking me for a list with data shared by these two classes.

This is the error:

Recursive datastructure. Add prefab values for one of the following types: CustomerView, List<YearConfigView>, YearConfigView

This is the @Test class:

@Test
    public void CustomerViewTest() {
EqualsVerifier.forClass(CustomerView.class).withRedefinedSuperclass().withGenericPrefabValues(CustomerView.class).verify();
        }

@Test
    public void YearConfigViewTest() {
        EqualsVerifier.forClass(YearConfigView.class).suppress(Warning.ALL_FIELDS_SHOULD_BE_USED).verify();
    }

CustomerView.java:

public class CustomerView extends EntityBase<Integer> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private List<YearConfigView> yearConfigs;

    @JsonProperty("current_year_config")
    public YearConfigView getCurrentYearConfig() {
        if (this.getYearConfigs() == null || this.getYearConfigs().isEmpty()) {
            return null;
        }
        int currentYear = LocalDate.now().getYear();
        return this.yearConfigs.parallelStream().filter(yc -> yc.getYear() == currentYear).findAny().orElse(null);
    }
}

YearConfigView.java:

public class YearConfigView extends EntityBase<Integer> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private CustomerView customer;
    private Integer year;
    private String comments;
}

My problem: What do I have to change or add in EqualsVerifier to solve the problem?

sinfryd95
  • 175
  • 1
  • 2
  • 8
  • Could you post the code of `CustomerView` and `YearConfigView` ? I suspect one of the fields in `CustomerView` or `YearConfigView` is of type itself – eHayik Mar 08 '21 at 17:42
  • They are ``entity`` classes, ``CustomerView`` collects the data from ``YearConfigView`` – sinfryd95 Mar 08 '21 at 17:48

1 Answers1

6

Creator of EqualsVerifier here.

Without seeing your classes (I'd like to see exactly which fields CustomerView and YearConfigView have, and how equals and hashCode are implemented on both classes), it's hard to say for certain what's going on, but I suspect it's this:

CustomerView has a reference to YearConfigView (or perhaps List<YearConfigView>), and YearConfigView has a reference to CustomerView.

EqualsVerifier, while doing its thing, tries to make instances of the classes it's verifying, and giving its fields proper values too. In order to do that, it must recursively instantiate the class's fields and give those values too. Usually, that's not a problem, but sometimes you run into a loop, like in your case: in order to create a value for CustomerView, it must have a value for YearConfigView and vice versa.

The way to avoid this, is by giving EqualsVerifier some 'prefab values'. I see you've already tried to do something like this, by adding .withGenericPrefabValues(CustomerView.class). (This method requires 2 parameters so I suspect you may have removed some code before posting it to StackOverflow ) This only works if CustomerView is itself a generic class, which I can't verify because you didn't post that particular piece of code. In any event, you shouldn't give generic prefab values or regular prefab values for the class you're testing.

In general, though, your tests should both give a prefab value for the other class. That would look like this:

@Test
public void CustomerViewTest() {
    YearConfigView one = new YearConfigView();
    one.setYear(2020);
    YearConfigView two = new YearConfigView();
    two.setYear(2021);

    EqualsVerifier.forClass(CustomerView.class)
        .withRedefinedSuperclass()
        .withPrefabValues(YearConfigView.class, one, two)
        .verify();
}

@Test
public void YearConfigViewTest() {
    CustomerView one = new CustomerView();
    one.setName("Alice");
    CustomerView two = new CustomerView();
    two.setName("Bob");

    EqualsVerifier.forClass(YearConfigView.class)
        .suppress(Warning.ALL_FIELDS_SHOULD_BE_USED)
        .withPrefabValues(CustomerView.class, one, two)
        .verify();
}

Note that I still don't know which fields are included in your equals methods, so I'm only making an educated guess about how to instantiate your classes.

For more information, see the relevant page in the EqualsVerifier documentation. Since the classes are JPA entities, this page might also be helpful: it explains how the @Id is treated by EqualsVerifier.

jqno
  • 15,133
  • 7
  • 57
  • 84
  • CustomerView and YearConfigView are entities. YearConfigView contains a series of fields that relate to CustomerView. CustomerView takes a list of the data from the YearConfigView and stores it in a column. CustomerView depends on the data in the YearConfigView. This is the CustomerView method: `private List yearConfigs;` I update the question with this data. – sinfryd95 Mar 09 '21 at 08:19
  • So, that means my assumption was correct. Note that EqualsVerifier only looks at fields and the `equals` and `hashCode` methods. It doesn't do anything with any other methods. The fact that it's a JPA entity is also irrelevant, except for the fact that EV will not complain about things that aren't final. – jqno Mar 09 '21 at 08:23
  • I have updated the question but I can't solve the problem with ``withPrefabValues``, should something change? What to declare in ``new YearConfigView (...);`` and ``new CustomerView (...) ;``? – sinfryd95 Mar 09 '21 at 08:26
  • You need to have two different instances of `YearConfigView` and two of `CustomerView`. I can't look inside your codebase; what do you usually do if you need an instance of such a class in a test? – jqno Mar 09 '21 at 08:32
  • This is what I do to instantiate it in other entities: ``private CustomerView currentCustomer; @JsonProperty("current_customer_id") public void setCurrentCustomerId(Integer idCustomer) { if (idCustomer == null) { this.currentCustomer = null; } else { this.currentCustomer = new CustomerView(); this.currentCustomer.setId(idCustomer); } }`` – sinfryd95 Mar 09 '21 at 08:38
  • Well then, you should do that here as well :) – jqno Mar 09 '21 at 08:40
  • Can you update the answer to see how to do it? because it is being complicated for me, I don't know why it fails. – sinfryd95 Mar 09 '21 at 08:43
  • And now that I equal ``one`` and ``two`` in both entities? – sinfryd95 Mar 09 '21 at 08:50
  • I've made an update with that, but really, creating instances of your own JPA entities is something you (and your co-workers) know most about. I can only make a guess, and that guess may very well be wrong. – jqno Mar 09 '21 at 08:51
  • No, `one` and `two` must _not_ be equal to each other, that's the whole point. Please also read the documentation I linked to at the end of my answer. – jqno Mar 09 '21 at 08:52
  • Ok now I get the following error: ``Suppress Warning.SURROGATE.KEY if you want to use only the @Id or @EmbeddedId field(s)``. I've tried ``.withIgnoredAnnotations(Entity.class, Id.class, Embeddable.class, MappedSuperclass.class, Transient.class)``, but it still doesn't work. What should I use in this case? – sinfryd95 Mar 09 '21 at 09:08
  • Apparently you're using the `id` field in your `equals` methods? Add a line `.suppress(Warning.SURROGATE_KEY)` to your call to EqualsVerifier. It's right there in the error message ;). Also, please read the docs I linked to in the answer, they explain in much more detail what's happening. – jqno Mar 09 '21 at 09:22
  • Ok that solves that error but now another one jumps: ``equals should not use name because Warning.SURROGATE_KEY is suppressed and it is not marked as @Id or @EmbeddedId, but it does``. This one I no longer know what it refers to, I have tried using withIgnoredAnnotations but it doesn't work I don't know what it might be this time – sinfryd95 Mar 09 '21 at 09:37
  • EqualsVerifier assumes `equals` is based on a natural key, i.e. the fields in your class *except* `id`. Suppressing the warning flips that around to a surrogate key, and now you can *only* use `id`. If (for some reason) you want to use both `id` and all the other fields, I guess you can ignore the annotations. Add `.withIgnoredAnnotations(Entity.class, Id.class, Embeddable.class, MappedSuperclass.class, Transient.class)` to each EqualsVerifier call, as described in the doc. – jqno Mar 09 '21 at 09:43
  • That is what I have used but it also causes me an error: `` make your class or your equas method final, or suplly an instance of a redifined subclass using widthRedefinedSubclass if equals cannot be final``. – sinfryd95 Mar 09 '21 at 09:48
  • Well... can you do that, then? Make `equals` and `hashCode` final? If not, the easiest way out is to add `.suppress(Warning.STRICT_INHERITANCE)`. – jqno Mar 09 '21 at 09:52
  • I have tried adding ``withRedefinedSubclass (CustomerView.class)`` but I get a different error: ``CustomerView (id = 1, name = one) equals subclass instance CustomerView (id = 1, name = one)``. Is it correct to use ``withRedefinedSubclass``? Am I using it well or should I change something? – sinfryd95 Mar 09 '21 at 09:52
  • Ok the problem is solved using ``withIgnoredFields`` I don't know if it is correct but it is the only way to get it ok – sinfryd95 Mar 09 '21 at 10:30
  • You shouldn't use `withRedefinedSubclass` unless you have designed both your class and your equals method for overriding: see https://jqno.nl/equalsverifier/manual/inheritance/ . I guess using `withIgnoredFields` is fine too, I can't really tell unless you share the `equals` and `hashCode` methods of your classes. In any event, I'm glad you managed to get it working: having it under test at all is the main thing! – jqno Mar 09 '21 at 11:25