This is an AssertJ specific question. Consider the tests below where two lists are compared first directly and then nested in some other object through usingRecursiveComparison().
public class RecursiveComparisonWithNestedListsTest {
#Test
void topLevelList() {
assertThat(List.of("Bob", "Chris"))
.containsExactlyElementsOf(List.of("Bob"));
}
#Test
void nestedList() {
assertThat(new Person("Amy", List.of("Bob", "Chris")))
.usingRecursiveComparison()
.isEqualTo(new Person("Amy", List.of("Bob")));
}
}
record Person(String name, List<String> friends) {
}
In the first case the message is more helpful, specifying the missing element:
Expecting actual:
["Bob", "Chris"]
to contain exactly (and in same order):
["Bob"]
but some elements were not expected:
["Chris"]
In the second case the message just mentions that the size differs:
Expecting actual:
Person[name=Amy, friends=[Bob, Chris]]
to be equal to:
Person[name=Amy, friends=[Bob]]
when recursively comparing field by field, but found the following difference:
field/property 'friends' differ:
- actual value : ["Bob", "Chris"]
- expected value: ["Bob"]
actual and expected values are collections of different size, actual size=2 when expected size=1
This is just a simple example; the actual use case involves more complex objects and much larger lists. In that context, specifying what item is missing from the list makes a big difference. This brings me to my question: Is there any way to configure the recursive comparison to compare lists just like in the first example?
I certainly have the workaround to ignore the list in the recursive comparison and then assert list equality separately, but I was wondering if there's some trick to get it all in one step.
No there is no such capability at the moment.
Related
I have this code, which obviously doesn't look nice - it seems all the if-else can somehow be avoided.
if(sortBy.equals("firstName"))
personList.sort(Comparator.comparing(Person::getFirstName));
else if(sortBy.equals("lastName"))
personList.sort(Comparator.comparing(Person::getLastName));
else if(sortBy.equals("age"))
personList.sort(Comparator.comparing(Person::getAge));
else if(sortBy.equals("city"))
personList.sort(Comparator.comparing(Person::getCity));
else if(sortBy.equals("state"))
personList.sort(Comparator.comparing(Person::getState));
else if(sortBy.equals("zipCode"))
personList.sort(Comparator.comparing(Person::getZipCode));
the function takes sortBy, which is the name of one of the attributes of a Person, and applies a sorting to a personList based on that field. How can I avoid the if-else and write a better looking, possibily one line code?
Currently I have found that I can use a HashMap to create a mapping between a field name and a corresponding comparator.
map.put("age", Comparator.comparing(Person::getAge));
map.put("firstName", Comparator.comparing(Person::getFirstName))
...
And use personList.sort(map.get(sortBy)).
But still felt like it can further be improved without an extra step, to the point where it follows the open-closed principle, and adding a new field to Person would not need us to modify the code. I'm looking for something like
personList.sort(Comparator.comparing(Person::getterOfField(sortBy)))
UPDATE-1
For now, I decided to stick with using a Map<String, Function<Person, Comparable<?>> and I do not like to consider reflection based solutions. But still searching if I can find a similar way as this one where sort is a parameter.
UPDATE-2
I think a one-liner is not a good solution, cuz you wouldn't get a compile time error if one of the fields does not implement Comparator.
In general java doesn't want you to work with it this way1; it is not a structurally typed language, and unlike e.g. javascript or python, objects aren't "hashmaps of strings to thingies".
Also, your request more fundamentally doesn't add up: You can't just go from "field name" to "sort on that": What if the field's type isn't inherently sortable (is not a subtype of Comparator<Self>?)
What if there is a column in whatever view we're talking about / config file that is 'generated'? Imagine you have a field LocalDate birthDate; but you have a column 'birth month'2. You can sort on birth month, no problem. However, given that it's a 'generated value' (not backed directly by a field, instead, derived from a calculation based on field(s)), you can't just sort on this. You can't even sort on the backing field (as that would sort by birth year first, not what you want), nor does 'backing field' make sense; what if the virtual column is based on multiple fields?
It is certainly possible that currently you aren't imagining either virtual columns or fields whose type isn't self-sortable and that therefore you want to deposit a rule that for this class, you close the door on these two notions until a pretty major refactor, but it goes to show perhaps why "java does not work that way" is in fact somewhat 'good' (closely meshes with real life concerns), and why your example isn't as boilerplatey as you may have initially thought: No, it is not, in fact, inevitable. Specifically, you seem to want:
There is an exact 1-to-1 match between 'column sort keys' and field names.
The strategy to deliver on the request to sort on a given column sort key is always the same: Take the column sort key. Find the field (it has the same name); now find its getter. Create a comparator based on comparing get calls; this getter returns a type that has a natural sorting order guaranteed.
Which are 2 non-obvious preconditions that seem to have gotten a bit lost. At any rate, a statement like:
if(sortBy.equals("firstName"))
personList.sort(Comparator.comparing(Person::getFirstName));
encodes these 2 non-obvious properties, and trivially, therefore means it is also possible to add virtual columns as well as sort keys that work differently (for example, sorts on birth month, or, sorts on some explicit comparator you write for this purpose. Or even sorts case insensitively; strings by default do not do that, you'd have to sort by String.CASE_INSENSITIVE_COMPARATOR instead.
It strikes me as a rather badly written app if a change request comes in with: "Hey, could you make the sort option that sorts on patient name be case insensitive?" and you go: "Hooo boy that'll be a personweek+ of refactoring work!", no?
But, if you insist, you have 2 broad options:
Reflection
Reflection lets you write code that programatically gets a list of field names, method names, and can also be used to programatically call them. You can fetch a list of method names and filter out everything except:
instance methods
with no arguments
whose name starts with get
And do a simple-ish get-prefix-to-sort-key conversion (basically, .substring(3) to lop off the get, then lowercase the first character, though note that the rules for getter to field name get contradictory if the first 'word' of the field is a single letter, such as getXAxis, where half of the beanspec documents say the field name is definitely XAxis, as xAxis would have become getxAxis, and the other half say it is ambiguous and could mean the field name is XAxis or xAxis).
It looks something like this:
// intentionally raw type!
Map comparators = new HashMap();
for (Method m : Person.class.getMethods()) {
if (Modifiers.isStatic(m.getModifiers()) continue;
if (m.getParameterCount() != 0) continue;
String n = m.getName();
if (!n.startsWith("get") || n.length() < 4) continue;
n = Character.toLowerCase(n.charAt(3)) + n.substring(4);
comparators.put(n, (a, b) -> {
Object aa = m.invoke(a);
Object bb = m.invoke(b);
return ((Comparable) aa).compareTo(bb);
});
}
MyClass.COMPARATORS = (Map<String, Comparator<?>>) Collections.unmodifiableMap(comparators);
Note how this causes a boatload of errors because you just chucked type checking out the window - there is no actual way to ensure that any given getter type actually is an appropriate Comparable. The warnings are correct and you have to ignore them, no fixing that, if you go by this route.
You also get a ton of checked exceptions issues that you'll have to deal with by catching them and rethrowing something appropriate; possibly RuntimeException or similar if you want to disregard the need to deal with them by callers (some RuntimeException is appropriate if you consider any attempt to add a field of a type that isn't naturally comparable 'a bug').
Annotation Processors
This is a lot more complicated: You can stick annotations on a method, and then have an annotation processor that sees these and generates a source file that does what you want. This is more flexible and more 'compile time checked', in that you can e.g. check that things are of an appropriate type, or add support for mentioning a class in the annotation that is an implementation of Comparable<T>, T being compatible with the type of the field you so annotate. You can also annotate methods themselves (e.g. a public Month getBirthMonth() method). I suggest you search the web for an annotation processor tutorial, it'd be a bit much to stuff an example in an SO answer. Expect to spend a few days learning and writing it, it won't be trivial.
[1] This is a largely objective statement. Falsifiable elements: There are no field-based 'lambda accessors'; no foo::fieldName support. Java does not support structural typing and there is no way to refer to things in the language by name alone, only by fully qualified name (you can let the compiler infer things, but the compiler always translates what you write to a fully "named" (package name, type name that the thing you are referring to is in, and finally the name of the method or field) and then sticks that in the class file).
[2] At least in the Netherlands it is somewhat common to split patient populations up by birth month (as a convenient way to split a population into 12 roughly equally sized, mostly arbitrary chunks) e.g. for inviting them in for a checkup or a flu shot or whatnot.
Assuming that the sortBy values and the corresponding getters are known at compile, this would be a good place to use a string switch statement:
Function<Person.String> getter = null;
switch (sortBy) {
case "firstName":
getter = Person::getFirstName; break;
case "lastName":
getter = Person::getLastName; break;
...
}
personList.sort(Comparator.comparing(getter));
If you use a recent version of Java (Java 12 and later) you could use a switch expression rather than a switch statement.
Function<Person.String> getter;
getter = switch (sortBy) {
case "firstName" -> Person::getFirstName;
case "lastName" -> Person::getLastName;
...
default -> null;
}
personList.sort(Comparator.comparing(getter));
Note: you should do a better job (than my dodgy code) of dealing with the case where the sortBy value is not recognized.
As keshlam suggested, I think using the reflection API is the best fitting answer to your question, but keep in mind that using it in production code is generally discouraged.
Note: if you add a new Person-attribute which isn't itself Comparable, you'll have to resort to a custom Comparator anyway. With that in mind, you might want to keep the Map<String, Comparator<?>> solution you already have.
I have been working with the following class named City
#ToString
#AllArgsConstructor
public class City {
Integer id;
String name;
}
and tried to convert it to a record called CityRecord as
record CityRecord(Integer id, String name) {} // much cleaner!
But moving to such a representation, one of our unit tests starts failing. The tests internally deal with a list of cities read from a JSON file and mapped to an object further counting the cities while grouping them under into a Map. Simplified to something like:
List<City> cities = List.of(
new City(1, "one"),
new City(2, "two"),
new City(3, "three"),
new City(2, "two"));
Map<City, Long> cityListMap = cities.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.counting()));
The above code asserted true to contain 4 keys and each accounting for 1 of its occurrence. With the record representation, there are no more than 3 keys in the resulting Map. What is causing this and what should be the way to go around this?
Cause
The reason behind the behavior observed is as documented in java.lang.Record
For all record classes, the following invariant must hold: if a record
R's components are c1, c2, ... cn, then if a record instance is copied
as follows:
R copy = new R(r.c1(), r.c2(), ..., r.cn()); then it must be the case that r.equals(copy).
In short, your CityRecord class now has an equals(and hashcode) implementation that compares the two attributes and ensure if they are equal the record consisting of those components are also equal. As a result of this evaluation, the two record objects with the same attributes would be grouped together.
The result, therefore, would be correct to infer/assert that there should be three such keys with the one having id=2, name="two" counted twice.
Immediate Remedy
An immediate temporary solution to this would be to create a custom(flawed - reason explained later) equals implementation within your record representation as well. This would look like:
record CityRecord(Integer id, String name) {
// WARNING, BROKEN CODE
// Does not adhere to contract of `Record::equals`
#Override
public boolean equals(Object o) {
return this == o;
}
#Override
public int hashCode() {
return System.identityHashCode(this);
}
}
Now that the comparison would be between two objects as in while using the existing City class, your tests would just work fine. But you must note the caution below before using any such remedy.
Caution
As the JEP-359 reads, Records are more like "data carrier" and while choosing to migrate your existing classes, you must be aware of the standard members acquired by a record automatically.
Planning to migrate one must be aware of the complete details of the current implementation, such as in the example you quoted while you've grouped by City, there should be no reason to have two cities with same id and name data to be listed differently. They should be equal, it should be the same data after all repeated twice and hence the correct counts.
In which case, your existing implementation if representing a data model could be rectified to match the record in a way by overwriting the equals implementation to account for comparing the individual attributes as well which is where the immediate remedy stated above is contradictory and should be avoided.
i have two lists as shown below.
List<Component> oldComps =getOldComps();
List<Component> newcomps=getNewComps();
corresponding POJOS are:
public class Component {
private List<Endpoint> endpoints;
}
public class Endpoint {
private String path;
}
i have a use case where i need to identify:
is both newComps and oldComps Lists have same data or not ?
is there any component added/deleted in newComps which is not there in oldcomps?if it is there that component details.
is there any endponit added/deleted in any of newComps list component which is not there in oldcomps list component?if it is there that endpoint details.
newComps
oldComps
for detailed exaple : as shown in attached images if oldComps and newComps
==>contains same data.
Based on the presumption you have hashcode correct.
HashSet oldComponentSet = new HashSet(oldComps);
HashSet newComponentSet = new HashSet(newComps);
//this line will give you intersection
oldComponentSet.retainAll(setTwo);
// this line will give you difference
newComponentSet.removeAll(oldComponentSet)
//united differfence based on presumption above two lines were not executed
oldComponentSet.removeAll(newComponentSet).add(newComponentSet.removeAll(oldComponentSet));
You can get the hash code of your data structure which is a built-in function on many modern programming language to compare with another one.
You can get the result by calling the hashCode() method from the object that you want. In your case, oldComps.hashCode() will give you the result.
For further: hashCode() by Baeldung
You can use .equals() method to compare the two lists.
To find any component which is not in the other list you can use .removeAll() method of List class to identify the unique element.
I am brand new to Java and i need some help. I have two lists.
List Expected = [Name , SSN, DOB];
List Actual = [Name , SSN];
I need to assert whether the List Actual values are available in List Expected, in other words, if both of the values in List Actual if it available in List Expected, my test should Pass. I googled and i tried to use this below assert statement. But my test is failing with the reason it is trying to validate the "DOB" from the List Expected.
Assert.assertThat("The Test Result Is.... : ", Expected, containsInAnyOrder(Actual.toArray()));
can anyone help me?
assertTrue(expected.containsAll(actual));
I strongly advise to stop using raw types (i.e. use List<String> and not List), and to respect the Java naming conventions (variables start with a lowercase letter).
I am connecting to two websites, taking the json from each site, and converting them into java objects. Now these objects are lists and I want to compare the two to see if if everything in list one is the same as everything in list two. I have tried the following but I get the error under the assertThat that I need to create a new method that contains assertThat, but I am just simply writing a junit test?
`
#Test
public void test_1() throws Exception
{
List<Sample> proj1 = getProject1();
List<Sample> proj2 = getProject2();
assertThat(proj1, hasItem(proj2));
}
`
It looks like it's because your hasItem(proj2) is actually passing a full list, not a single item to evaluate.
For example, if you change it to:
assertThat(proj1, hasItem(proj2.get(0)));
It should be a valid method call
If you want to compare a list of items then this may help: http://junit.sourceforge.net/javadoc/org/junit/matchers/JUnitMatchers.html