Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spock with Strong Types #1279

Closed
Richargh opened this issue Feb 20, 2021 · 6 comments · Fixed by #1280
Closed

Spock with Strong Types #1279

Richargh opened this issue Feb 20, 2021 · 6 comments · Fixed by #1280

Comments

@Richargh
Copy link

Richargh commented Feb 20, 2021

Issue description

Is there a way to create data-driven tests with Spock that use explicit or implicit type conversion?

In my sample project I have classes like Age or Name that wrap Integer and String plus a Person { Name name; Age age; } that uses them. I'd like to write my data like so:

def "a test"(Name name, Age age){
    where:
    name      | age
    "John"    | 5
    "Lisa"     | 2
}

or like this:

def "a test"(Person person){
    where:
    name      | age
    "John"    | 5
    "Lisa"     | 2
}

What I don't want to do is to write it like this, because that creates a lot of noise in the test:

def "a test"(Name name, Age age){
    where:
    name                   | age
    Name.of("John") | new Age(5)
    Name.of("Lisa")  | new Age(2)
}

I've written a Groovy type coercion extension method from String to Person but Spock does not want to use it.

JUnit5 automatically uses a static factory for type conversion or I can supply an ArgumentConverter/ArgumentAggregator. Can I do something similar in Spock?

How to reproduce

Link to a gist or similar (optional)

I created a sample test if you want to take a look.

Additional Environment information

  • Java 11
  • Spock: "2.0-M4-groovy-3.0"
  • Groovy: "3.0.7"
  • Gradle 6.8.2
  • Mac OS X
@Vampire
Copy link
Member

Vampire commented Feb 20, 2021

Supporting the type coercion or rather why it is not supported is probably worth looking at.
What you currently could do, is to write the test like this:

def "a test"(Name name, Age age) {
    where:
    theName | theAge
    "John"  | 5
    "Lisa"  | 2

    name = Name.of(theName)
    age = new Age(theAge)
}

@Vampire
Copy link
Member

Vampire commented Feb 20, 2021

You could also write a global or annotation based extension that registers a feature method interceptor and adjusts as needed, like

    @MakePerson
    def "a test"(Person person) {
        println(person)
        where:
        name    | age
        "John"  | 5
        "Lisa"  | 2
    }

// ...

@Retention(RUNTIME)
@Target(METHOD)
@ExtensionAnnotation(MakePersonExtension)
@interface MakePerson {
}

class MakePersonExtension implements IAnnotationDrivenExtension<MakePerson> {
    @Override
    void visitFeatureAnnotation(MakePerson annotation, FeatureInfo feature) {
        feature.featureMethod.addInterceptor(new IMethodInterceptor() {
            @Override
            void intercept(IMethodInvocation invocation) throws Throwable {
                invocation.arguments[2] = person { it.withName(invocation.arguments[0]).withAge(invocation.arguments[1]) }
                invocation.proceed()
            }
        })
    }
}

In reality you would probably do some magic like checking whether there is an unset Person parameter and whether there are parameters named name and age and then use those to construct the person.

@Vampire
Copy link
Member

Vampire commented Feb 21, 2021

Or a variant of the first version that also works:

def "a test"(Name name, Age age) {
    where:
    theName | theAge
    "John"  | 5
    "Lisa"  | 2

    name = theName as Name
    age = theAge as Age
}

@Vampire
Copy link
Member

Vampire commented Feb 21, 2021

And with my PR #1280 it will simply work as you expected.
It works both, with defining an extension module like you did, or with the help of the @Use extension.
But be aware that you have to annotate the specification, as the @Use extension cannot work on the code in the where block due to technical reasons as documented.

If you want to combine name and age into a Person parameter, you still have to go the Spock extension way though.

@leonard84
Copy link
Member

leonard84 commented Feb 22, 2021

If you want to combine name and age into a Person parameter, you still have to go the Spock extension way though.

Alternatively, you could use a map if the object has a no args constructor, or add a custom asType to handle a other constructor.
Not as pretty, but easier than writing a custom extension.

@Richargh
Copy link
Author

Wow. Thank you so much for your fast response and I'm sorry for my tardiness when it comes to replying to the issue I created.

Thanks for your replies. I've incorporated your code suggestions into my example project. I hope it's ok that I directly copied your extension code @Vampire and added an attribution?

I eagerly look forward to the next Spock version that incorporates type coercion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants