A little bit of brainstorming here. In testing, s...
# test
d
A little bit of brainstorming here. In testing, specifically when comparing expected and actual values, I would like to ignore certain fields located deep in a nested data class structure. I know I can do that with assertj using
Copy code
assertThat(actual).usingRecursiveComparison().ignoringFields("my.nested.field", MyClass::otherField.name).isEqualTo(expected)
but that is unsatisfactory for two reasons: • The ignore list is either not type-safe (plain strings) or is too verbose (property references) • The ignore list needs to be specified on the call site instead of on the declaration side. The latter point is important in the case of test fixtures which are shared among many tests and which contain some dynamic or computed data, such as dates or hashes. In such cases it would be much more convenient for me if I could ignore the field "on the declaration side, e.g. like this:
Copy code
my TEST_DATA = MyClass(fixedField = TEST_VALUE, dynamicField = any())
and then use some special assertion function that understands that
any()
. For that to work, I imagine that the return type of
any()
should be both a subclass of the
dynamicField
type as well some marker type for the assertion function. E.g. in pseudo code
Copy code
interface AnyMarker

inline fun <reified T> any() = object : T, AnyMarker {}
This looks very similar to how Spring creates proxy classes. That relies on the all-open compiler plugin. But I can't use a similar trick here because • It would be quite impractical to mark a class with some annotation for the plugin only for the testing use case • I'm not even sure I can "open" classes such as
String
which would be needed for the important ignore-hash use case. My questions are: • Does this idea make sense? • Is it feasible? If yes, how to effectively implement
any()
(or something similar)?
e
you definitely can't "open" String or mess with anything on the bootclasspath
s
Not sure about the entire idea but for some of the jre classes (String, Date, Integer, etc.) you should be able to allocate new special instances and then in the equals compare by identity to identify marker instances. Quite hacky but maybe it will give you enough rope to be useful.
c
This seems very hacky. Could this be a XY problem? Do you really need to ignore these values specifically for testing, or do you actually want to ignore them all the time (e.g. have a custom
equals
)?
d
In this particular case, custom equals would probably work as well (although I'm not sure that it would in general). However, AFAIK it would require specifying all the other fields in the equals method which is quite verbose and a maintenance burden. I understand that in statically typed language my idea might seem like a hack, and that's exactly one of the few instances where I miss the flexibility of dynamically typed languages 🙂
Hmm, now I realize that even custom equals wouldn't work since some of the affected classes are generated 😞
c
Ah, well. Once you start using generated code, all bets are off…
d
@spand Could you share more details about your proposal? E.g. how to specify a special for String?
e
I assume they meant
Copy code
val MY_ANY_STRING = buildString { ... } // new non-interned non-pooled instance

fun myEquals(a: Any?, b: Any?): Boolean {
    if (a === MY_ANY_STRING || b === MY_ANY_STRING) return true
it's not going to work out for primitive ints or other value types though