Search code examples
groovyspockgeb

Geb+Spock tests can't reuse content c'tors, fail with MissingMethodException


I've only recently started creating Geb/Spock tests for our web app and -- given my limited knowledge of Geb, Spock, and all things Groovy -- have bumped into an error that makes no sense whatsoever (given my Java experience).

So, here is the module in question:

import geb.Module
import geb.navigator.Navigator

class Tile extends Module {

    def currencyPair

    static content = {

        amount { 
            $(".currencypair-span", text: containsWord(currencyPair))
                .parent().parent()
                .find(".tile-amount-setup").find("input") }     
    }       
 }

Nothing extraordinary there.

Here is the page (I'm not sure how this all fits together, though):

import geb.Page

class TraderApp extends Page {

    static url = "./"

    static at = { title == "FOOBARTrader" }

    static content = {
        tile { instrument -> module Tile, currencyPair: instrument }
    }

}

Here is the test spec:

import geb.spock.GebSpec

import spock.lang.*
import org.openqa.selenium.Keys

class BugSpec extends SbkSpec {

    final CURPAIR = "FOOBAR"

    def setupSpec() {
        accountSelector.dropDown.click()

        accountSelector.dropDown << Keys.chord(Keys.CONTROL, "a")
        accountSelector.dropDown << "FxOnly"
        accountSelector.dropDown << Keys.ENTER

        waitFor() { accountSelector.dropDown.value() == "FXOnly" }

        tileLayout("Majors").tab.click()
    }

    def "test1"() {
        given:
            tile(CURPAIR).amount.click()

        when: 
            println("foo")

        then:
            waitFor {true}            
    }

    def "test2"() {
        given:
            tile(CURPAIR).amount.click()

        when: println("bar")

        then:
            waitFor {true} 
    }
}

Now, the issue is that the first test will run and pass, but when the second test is run the JVM will report a NoSuchMethodException on the tile() method.

How is this possible? I appreciate that Groovy is a dynamic language, but where is the method disappearing to?

This makes it impossible for me to reuse content in several different tests, as the content in question is unreachable once I exit the first test that uses it.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running BugSpec
Starting ChromeDriver (v2.10.267521) on port 32075
Only local connections are allowed.
foo
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 19.425 sec <<< FAILURE!
test2(BugSpec)  Time elapsed: 0.175 sec  <<< ERROR!
groovy.lang.MissingMethodException: No signature of method: geb.navigator.NonEmptyNavigator.tile() is applicable for argument types: (java.lang.String) values: [FOOBAR]
Possible solutions: size(), tail(), filter(java.lang.String), find(java.lang.String), is(java.lang.String), take(int)
        at geb.navigator.NonEmptyNavigator.methodMissing(NonEmptyNavigator.groovy:463)
        at geb.content.NavigableSupport.methodMissing(NavigableSupport.groovy:123)
        at geb.Browser.methodMissing(Browser.groovy:194)
        at geb.spock.GebSpec.methodMissing(GebSpec.groovy:51)
        at BugSpec.test2(BugSpec.groovy:35)

Help please?

EDIT:

As per Erdi's answer, included is also the superclass of BugSpec:

import geb.spock.GebReportingSpec

class SbkSpec extends GebReportingSpec {

    def setupSpec() {
        def environment = System.getProperty("geb.env");

        given:
            to AuthApp                

        expect:
            at AuthApp                

        when:
            def username = browser.getConfig().getRawConfig().get("username")
            def password = browser.getConfig().getRawConfig().get("password")

            login.username.value(username)
            login.password.value(password)

            login.loginButton.click()

        then:
            at TraderApp
            and:
                waitFor() { accountSelector.dropDown.value() == "FOOBAR-ACCOUNT" }
    }

    def cleanupSpec() {
        given:
            at TraderApp
        then:
            logout.logoutButton.click()
    }        
}

Solution

  • When using the built-in base GebSpec/GebReportingSpec Geb dispatches missing method calls to the page property of spec's browser property. Something (without seeing more code it's impossible to say what exactly it is) sets that property to an instance of TraderApp before the first test. Geb recreates the browser instance between each test which means that the page instance is reset to the default which is an instance of Page. If you set the page to be an instance of TraderApp then tile() method will be found:

    def "test2"() {
        given:
            page TraderApp
            tile(CURPAIR).amount.click()
    
        when: println("bar")
    
        then:
            waitFor {true} 
    }
    

    It would be way easier to reason about things and help out if you provided code for the chain of all superclasses of BugSpec up to the one that extends GebSpec/GebReportingSpec. Looks to me like you might be dealing with a smell where the tests are setup in way where their execution order matters.