Is there a way to prevent Geb from returning null from void methods?
Asked Answered
H

2

6

In a Spock Specification any line in expect: or then: block is evaluated and asserted as boolean, unless it has signature with return type void.

I've noticed that there is something odd going on for methods declared as void on any class inheriting from Navigator (for example Page or Module classes).

Let say we have such example:

class NavigationSpec extends GebSpec {

    def 'Collections page has a right header'() {

        when:
            to CollectionsPage

        then:
            hasHeaderText('Collections')
    }
}

The hasHeaderText() method is defined within CollectionsPage class as follows:

class CollectionsPage extends Page {

    static url = 'movies/collections'

    void hasHeaderText(String expectedText) {
        assert true
    }
}

On purpose I'm just asserting true over there so it should never fail. Even though it fails with an error:

Condition not satisfied:

hasHeaderText('Collections')
|
null

How and why a void method call result is evaluated as null?

I know how to 'fix it'. It's enough to declare the method return type as boolean and return true. This is ugly though as following all the asserts otherwise unnecessary return true has to be added like:

boolean hasHeaderText(String expectedText) {
    assert header.text() == expectedText
    return true
}

This causes only noise though. Is there any way to prevent Geb from returning null from void methods?

I'm, of course, aware that this specific case could go like:

boolean hasHeaderText(String expectedText) {
    header.text() == expectedText`
}

This doesn't explain the oddity of lost void return type, not to mention we loose meaningful assert failure message with such approach.

Hydra answered 11/4, 2016 at 12:55 Comment(0)
O
4

It's part of the Groovy language that every method returns a value. This allows any method to be used in expressions or as lambdas.

All methods that are declared void return null.

If you don't explicitly have any return statement, the result of the last expression in your method is returned.

You can look at the bytecode... even if you declare a return type, you don't actually need to return anything as Groovy will, by default, return null:

// returns null
String callMe() { }

static void main(args) {
    def x = callMe()
    assert x == null
    println "OK!"
}

Because Spock will assert anything in the then block which is not a simple assignment, you need to avoid doing anything other than boolean assertions in the then block. Even assigning a variable, though allowed, should be avoided... It's hard to keep tests clean and clear, and by adhering to these guidelines will really work for you in the long run, not against you.

So, the correct way to write the assertion you want is to make your method return a boolean:

boolean hasHeaderText(String expectedText) {
    header.text() == expectedText
}

And use it in the then block:

then: 'The header has the expected text #expectedText'
hasHeaderText expectedText

Looks pretty good if you ask me.

EDIT

I've noticed that Groovy/Spock actually will not assert the result of a normal void method even in the then block... What's probably going on here is that you don't have a normal void method, you seem to be calling a dynamic method of CollectionsPage (I guess that's Geb's magic in play), which means that, probably, the Spock AST Transformer does not have the opportunity to check the signature of the method you're calling, so it correctly assumes it must assert the result... at least that's what it looks like.

Olia answered 11/4, 2016 at 17:8 Comment(1)
Thanks for your answer. I'm aware of the things you wrote, especially the part in 'edit' notes. Indeed void method should not be evaluated as assert in spock's then block. Page's method is declared normal way, but that's my assumption as well, that Geb does some AST magic with it and thus breaks the contract. Using booleans with expectedText outside of the method is far from what I would like to achieve. Power assert elaborated failure messages are lost (think about comparing two maps for example) and text has to be repeated each time the method is used (DRY). I consider this to be a Geb bugHydra
H
3

@Renato is correct in the edited part of his response - your call to a void method gets asserted by Spock because it's a dynamic call and Spock isn't able to figure out that you are calling a void method and eagerly asserts the call. If you changed your code to:

class NavigationSpec extends GebSpec {

    def 'Collections page has a right header'() {
        when:
            CollectionsPage collectionsPage = to CollectionsPage

        then:
            collectionsPage.hasHeaderText('Collections')
    }

}

then it won't get asserted.

Honig answered 12/4, 2016 at 19:26 Comment(5)
Thanks for the answer. I was pretty sure that this is because of some dynamic nature of the call. Oddly I didn't catch it works well if invoked directly on the owning object. By the way, which answer I should accept now? @Olia was first while yours gives more details :)Hydra
Probably @Olia deserves an accept as his response was the first correct one. :)Honig
You're too humble. @Olia response was just a generic confirmation of my own assumptions that it is 'some dynamic magic' but there were neither any details provided of what's going on nor a solution. Thus, I'm accepting yours. I hope he doesn't mind...Hydra
That's fine by me :)Honig
in case you're still waiting 1 year later, it's fine by me if you accept @Honig answer :)Olia

© 2022 - 2024 — McMap. All rights reserved.