1

It seems that @RequiredArgsConstructor is not working in the code below - but only in a test using Spock framework, and only for a field which is of type of interface Dao.
Strictly speaking - the code is working while it shouldn't work in my opinion, taking into account that similar test under JUnit5 doesn't compile at all.

Could someone explain is it a bug, or a feature ?

package brumba;
public interface Dao {
    Integer getValueFor(Integer value);
}

package brumba;

import com.sun.istack.internal.NotNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class Brumba {

    @NotNull
    final private Dao dao;

//  If you uncomment the below 2 lines, then the test fails
//    @NotNull
//    final private String name;

    public Integer twice(Integer x){
        return x * 2;
    }

    public Integer twiceDao(Integer x){
        return dao.getValueFor(x);
    }
}

The below code works fine - but only in Spock (a similar test under JUnit5 doesn't compile).
It seems that Spock test somehow sees a default no-args constructor (while JUnit test doesn't see this constructor)
But when the 2 commented lines above were uncommented, then the test failed with the below error:

groovy.lang.GroovyRuntimeException: Could not find matching constructor for: brumba.Brumba()

package brumba

import spock.lang.Specification

class BrumbaTest extends Specification {

    def "twice should multiply argument by 2"() {
        given:
            def testedObject = new Brumba();

        expect:
            y == testedObject.twice( x )

        where:
            x | y
            0 | 0
            1 | 2
            2 | 4
            3 | 6
    }
}

And this JUnit test doesn't compile at all:

package brumba;

class BrumbaJUnit5Test {

    @org.junit.jupiter.api.Test
    void shouldTwice() {
        Brumba br = new Brumba();
    }
} 

the error is:

Error:(7, 21) java: constructor Brumba in class brumba.Brumba cannot be applied to given types;
  required: brumba.Dao,java.lang.String
  found: no arguments

Here are the dependencies I am using for this project:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.2-groovy-2.5</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.4</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.0-M1</version>
    <scope>test</scope>
</dependency>

Pradip Karki
  • 662
  • 1
  • 8
  • 21
krokodilko
  • 35,300
  • 7
  • 55
  • 79
  • i don't see `NoArgConstructor` in `public class Brumba` – Ryuzaki L Dec 23 '18 at 16:27
  • @Deadpool you can't see that - java compiler atomatically creates default no-arg constructor unless there is another constructor definied. Lombok `@RequiredArgsConstructor` should generate `public Brumba(Dao dao)` constructor, so in that case the class should have not this default constructor at all (it should be private - hidden) - but Spock test somehow sees this constructor and is able to instatiate an object of this class using this constructor. – krokodilko Dec 23 '18 at 16:36
  • i got you but i don't have idea on spock – Ryuzaki L Dec 23 '18 at 16:58

1 Answers1

1

First of all, I can confirm that this happens for me, too. I never noticed before.

I had to debug through source code and look at decompiled files in order to understand at least a bit what is going on here. I can tell you a few things:

  • This is unrelated to Lombok. It also happens to any Java class with a single-argument constructor taking an object type (i.e. not a primitive like int), e.g. String or your Dao.
  • It is unrelated to Spock because it also happens outside of Spock.
  • It seems to be related to dynamic Groovy runtime features.
  • I would rather call it a subtle bug than a feature, but I am not sure.

Java class:

package de.scrum_master.stackoverflow;

public class Brumba {
  public Brumba(String name) {}
}

Groovy class:

package de.scrum_master.stackoverflow

class BrumbaApp {
  static void main(String[] args) {
    new Brumba()
  }
}

Decompiled Groovy class:

package de.scrum_master.stackoverflow;

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class BrumbaApp implements GroovyObject {
  public BrumbaApp() {
    CallSite[] var1 = $getCallSiteArray();
    MetaClass var2 = this.$getStaticMetaClass();
    this.metaClass = var2;
  }

  public static void main(String... args) {
    CallSite[] var1 = $getCallSiteArray();
    var1[0].callConstructor(Brumba.class);
  }
}

The Groovy runtime class CallSite is actually an interface, but there is AbstractCallSite implementing it. If we look at this method

public Object callConstructor(Object receiver) throws Throwable {
    return callConstructor(receiver, CallSiteArray.NOPARAM);
}

and this definition

public final class CallSiteArray {
    // ...
    public static final Object [] NOPARAM = new Object[0];
    // ...

we understand that actually this method will be called

public Object callConstructor(Object receiver, Object[] args) throws Throwable {
    return CallSiteArray.defaultCallConstructor(this, receiver, args);
}

and so forth. I think what happens is that the Object[] of size 0 will be passed through as a constructor parameter somehow and the absence of an element interpreted as a null argument. This is also what you see in a debugger after object instantiation if like in your code the parameter is assigned to a member: The member will have the value null.

kriegaex
  • 63,017
  • 15
  • 111
  • 202