2

Suppose I have code that assigns a method to a Runnable, for example:

class C1{
    Runnable r;
}

class C2{
    void f(){}
}

var c1 = new C1();
var c2 = new C2();
c1.r = c2::f;

I want to write a unit test to assert that r is indeed assigned f, perhaps like this:

assertThat(c1.r).isEqualTo(c2::f); // Error: object is not a functional interface

Casting to Runnable does not help:

expected: ...$$Lambda$302/0x0000000800cd3518@1fe20588
but was : ...$$Lambda$301/0x0000000800c9fac0@77167fb7

In C# I can assign a method to an Action variable this assertion works. What's the equivalent in Java?

I am open to switching from Runnable to some other type, if that helps.

To give some more context: I'm trying to implement event-based decopuling like Arlo Belshee describes here: https://arlobelshee.com/decoupled-design/. Here's my C# implementation for comparison: https://jay.bazuzi.com/Decoupled-Design/

Jay Bazuzi
  • 45,157
  • 15
  • 111
  • 168
  • Well, the first thing to do is check if your IDE will automatically generate these tests. Most do. Look at JUnit test framework. – markspace Jul 02 '22 at 21:27
  • Some examples: https://stackoverflow.com/questions/9772618/writing-junit-tests – markspace Jul 02 '22 at 21:28
  • Various `assert` methods. I think this documentation is kinda old though. https://junit.org/junit4/javadoc/4.13/org/junit/Assert.html – markspace Jul 02 '22 at 21:29
  • 1
    Another approach would be to create a spy on an instance `c2` of `C2`, pass this spy along to the unit under test and then assert that `c2::F` was called. Yet another possibility would be to not assert that `c2::F` was called, but that the expected functionality (behaviour) was executed. – Turing85 Jul 02 '22 at 21:33
  • You want to assure that `C1::R` is a `Runnable`, or do you need to know that exactly `C2::F` is assigned to `C1::R`? And, by the way, `F` and `R` should be lowercase, according to the common coding conventions for Java. – tquadrat Jul 02 '22 at 21:36
  • 2
    It doesn't work the same way in Java as in C# (unfortunately), so if it's possible through some trickery, it's bound to be brittle and clumsy. Also, I'd write the test so it tests behaviour rather than implementation. Really a poor test issue. – Kayaman Jul 02 '22 at 21:36
  • 1
    @Kayaman: for my education, could you elaborate on what you just posted? – Hovercraft Full Of Eels Jul 02 '22 at 21:38
  • I like the idea of testing behavior (whether the right result is obtained) rather than testing whether `=` (assignment) did the right thing. – markspace Jul 02 '22 at 21:39
  • 1
    @HovercraftFullOfEels I meant C# having methods as first class objects vs. Java method references make the equality comparison break, and at that point it's not even worth (IMHO) to start playing with `LambdaMetaFactory` or whatever class might possibly help us duplicate the C# behaviour. Especially since tests should not test implementation like that. – Kayaman Jul 02 '22 at 21:42
  • @markspace – From the information what we have right now, the assignment IS the behaviour … – tquadrat Jul 02 '22 at 21:42
  • I understand but, the OP is basically asking if the compiler knows what it's doing. I guess I'm biased that I program in Java, and asking something I know the language isn't built to do seems unreasonable. – markspace Jul 02 '22 at 21:43
  • @tquadrat eventually the assigned variable will be used for something, so the assignment can't really be *the* behaviour. We can modify the test to do assignment + behaviour, and you can do assertions on something more useful maybe. This may be idiomatic C#, but it won't work in Java. – Kayaman Jul 02 '22 at 21:48
  • 1
    Adding to @Kayaman's comment... in my opinion, it depends whether `C1` and `C2` are tightly or loosely coupled. If they are tightly coupled, i.e. `C1` will always use `C2::F`, I would reommend writing a behavioural test. If they are loosely coupled, I would recommend wiriting a contract test, i.e. assert that `C1` calls whatever `Runnable` has been passed along (this approach requires IoC so that we can pass along some test-`Runnable` to `C1` and verify that it was called). – Turing85 Jul 02 '22 at 22:13
  • @Kayaman – The keyword in your comment is "eventually". If we had more context what the code should do, I would agree with you, but that context is missing. – tquadrat Jul 02 '22 at 22:15
  • @tquadrat I have added motivation. – Jay Bazuzi Jul 03 '22 at 02:32

2 Answers2

2

You're out of luck trying to compare lambdas in any sane way. The code may work in C# and other languages, but it's not worth trying to shoehorn it into Java.

I don't know the purpose of this exercise (as in is it just to test the viability of this in Java or if this is a pattern you regularly use), but generally translating from one language to another doesn't make sense unless there's an obvious compatibility. C# may look like Java (for some weird reason, hello J#), but there's a world of difference making many things completely distinct (e.g. async/await, only superficial similarity between LINQ and Java streams, methods not being truly first class objects in Java, and most likely many more as my C# knowledge is very limited).

IMHO the testing in the example was the weirdest part. It gives a low ROI to test microimplementation like that. I'm not sure if that is useful in GUI testing, but just as if you had a solution that involved metaprogramming, you couldn't just implement it in Java.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
1

The thing is that you create a new method reference each time with ::. Thus

var c1 = new C1();
var c2 = new C2();

Runnable r = c2::F;
c1.R = r;

System.out.println(c1.R == r);

will print true, because you assign and compare with the same runnable.

vladtkachuk
  • 656
  • 4
  • 13
  • While this is true, this approach has a major drawback in case `C1` and `C2` are loosely coupled: If we use an DI framework (e.g. Spring-DI or a CDI implementation like weld), the framework may create a proxy (depending on the testing framework/-setup we use, we can use the DI framework for the tests). This, in return, may lead to a situation where a proxy instead of `c2::F` is assigned to `c1.R`. A behavioural- or contract-test can circumvent this detail. – Turing85 Jul 02 '22 at 22:25