Skip to content

tomaslin/grails-test-recipes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 

Repository files navigation

#Recipes for Testing Grails

##The Command Line

To enter the command line, type 'grails' in your terminal.

####Run all tests

test-app

####Re-run failing tests

test-app -rerun

####Run only a test based on their phase:

test-app unit:
test-app integration:
test-app functional:
test-app other:

####Run tests that match filenames:

test-app A*
test-app A* B* C*

####Run tests that match package names:

test-app com.mypackage.*
test-app com.mypackage.**.*

####Run tests based on type:

test-app :spock

####Restarting test daemon

restart-daemon

####View results of test

open test-report

####Grails test Cheatsheet http://zanthrash.com/grailstesting/UnitTestingCheatSheet.pdf http://slid.es/colinharrington/testing-grails

####Configuring the test daemon The test deamon in the grails shell exists in 2.3.x branches to make running of the tests faster. This is similar to the gradle daemon.

To disable this functionality, modify the BuildConfig.groovy file and change the last attribute of this map to false

    test: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, daemon:true],

####Printing out which tests are running

Add the following script to scripts/_Events.groovy

eventTestCaseStart = { name ->
    println '-' * 60
    println "|$name : started"
}

eventTestCaseEnd = { name, err, out ->
    println "\n|$name : finished"
}

####Sharding tests for CI servers

Add the following to scripts/TestParallel.groovy

import org.codehaus.groovy.grails.test.*
import org.codehaus.groovy.grails.test.support.*
import org.codehaus.groovy.grails.test.event.*

includeTargets << grailsScript("_GrailsClean")
includeTargets << grailsScript("_GrailsTest")

target(main: "Runs functional tests in parallel in sets of bucketSize") {

    depends(checkVersion, configureProxy, parseArguments, cleanTestReports)

    def tests = new SpecFinder(binding).getTestClassNames()
    def id = (argsMap.set ?: 0) as int
    def sets = (argsMap.total ?: 4) as int

    List<String> shard = []

    tests.eachWithIndex { test, index ->
        if (index % sets == id) {
             shard << "${ tests.get(index) }"
        }
    }

    testNames = shard

    phasesToRun = ['functional']

    allTests()

}

setDefaultTarget(main)

class SpecFinder extends GrailsTestTypeSupport {

    SpecFinder(binding) {
        super('name', 'functional')
        buildBinding = binding
    }

    int doPrepare() {
        0
    }

    GrailsTestTypeResult doRun(GrailsTestEventPublisher eventPublisher) {
        null
    }

    def getTestClassNames() {
        findSourceFiles(new GrailsTestTargetPattern('**.*Spec')).sort { -it.length() }.collect { sourceFileToClassName(it) }
    }
}

To run, call grails test test-Parallel --set=2 --total=6

where set is the shard number and total is the total number of shards.

Be aware that you need to set the environment or it will default to Development.

Update: There is also grails plugin for test partition - http://grails.org/plugin/partition-tests

####Running tests in parallel (Secret Escapes)

Partitions tests and runs them in different processes

import groovy.sql.*
import org.codehaus.groovy.grails.test.*
import org.codehaus.groovy.grails.test.support.*
import org.codehaus.groovy.grails.test.event.*
 
includeTargets << grailsScript("TestApp")
 
target(main: "Runs functional tests in parallel in sets of bucketSize") {
    def reportsDir = 'reports'
    def numberOfServers = 5
 
    def sql = Sql.newInstance('jdbc:mysql://localhost:3306/', 'root', '', 'com.mysql.jdbc.Driver')
 
    def tests = new SpecFinder(binding).getTestClassNames()
    new File(reportsDir).mkdirs()
    def commands = []
    def threads = []
    def results = ''
 
    numberOfServers.times { id ->
 
        def reportsFile = new File(reportsDir + '/' + 'test' + id).absolutePath
 
        sql.execute( "DROP DATABASE IF EXISTS parallelDB${id};" )
        sql.execute( "CREATE DATABASE parallelDB${id};" )
 
        def pattern = ''
 
        tests.eachWithIndex { test, index ->
 
            if (index % numberOfServers == id)
            {
                pattern += " ${ tests.get(index) }"
            }
 
        }
 
        def command = "grails -Dgrails.project.test.reports.dir=${reportsFile} -Dserver.port=909${id} -Ddb.name=parallelDB${id} test-app functional:  ${pattern}"
 
        threads << Thread.start {
 
            println command
            ProcessBuilder builder = new ProcessBuilder(command.split(' '));
 
            builder.redirectErrorStream(true);
            Process process = builder.start();
 
            InputStream stdout = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));
 
            while ((line = reader.readLine()) != null)
            {
                if( !line.contains( 'buildtestdata.DomainInstanceBuilder' ) ){
                    System.out.println("Server ${id}: " + line);
                }
 
                if( line.contains( 'Tests passed:' ) || line.contains( 'Tests failed:' ) ){
                    results += "Server ${id}: " + line + '\n'
                }
            }
 
        }
 
    }
 
    threads.each {
        it.join()
    }
 
    println '------------------------------------'
    println 'Tests FINISHED'
    println '------------------------------------'
    println results
 
}
 
setDefaultTarget(main)
 
class SpecFinder extends GrailsTestTypeSupport {
 
    SpecFinder(binding) {
        super('name', 'functional')
        buildBinding = binding
    }
 
    int doPrepare() {
        0
    }
 
    GrailsTestTypeResult doRun(GrailsTestEventPublisher eventPublisher) {
        null
    }
 
    def getTestClassNames() {
        findSourceFiles(new GrailsTestTargetPattern('**.*Spec')).sort{ -it.length() }.collect{ sourceFileToClassName(it) }
    }
}

##Taglibs

####Mocking a view in unit tests

void "constraints are passed to template"() {
    	given:
		views["/_fields/default/_field.gsp"] = 'nullable=${constraints.nullable}, blank=${constraints.blank}'

		expect:
		applyTemplate('<f:field bean="personInstance" property="name"/>', [personInstance: personInstance]) == "nullable=false, blank=false"
}

####Setting pageScope variables

void testTagThatAccessesPageScope() {
        tagLib.pageScope.food = 'tacos'
        tagLib.tagThatAccessesPageScope([:])
        assertEquals 'food is tacos', tagLib.out.toString()
    }
}

class PagePropertyTagLib {
    
    def tagThatAccessesPageScope = {
        out << "food is ${pageScope.food}"
    }

}

###Testing Grails

Colin Harrington - Testing Grails : Experiences from the field - http://springone2gx.com/s/slides/2011/speaker/Colin_Harrington/testing_grails___experiencies_from_the_field/grails_testing.pdf Colin Harrington - Grails testing seminar : http://www.youtube.com/watch?v=RZlXFR013hg How to Make your Testing more Groovy - http://www.slideshare.net/smithcdau/how-to-make-your-testing-more-groovy

##Spock

( Mostly from: http://docs.spockframework.org/en/latest )

####Configuration ( Next level spock )

Spock has a configuration API.

Looks for (in order)…

-Dspock.configuration=path/to/file SpockConfig.groovy file on class path ~/.spock/SpockConfig.groovy

runner {
    filterStackTrace true
    optimizeRunOrder false
    include Database
    exclude UI
}

####Compare with previous value using old()

when: 
  value * 30
then:
  value == old( value ) * 30

####Changing label formatting in IntelliJ Idea

####Generate business friendly test run report

Spock subproject - https://github.com/spockframework/spock/tree/groovy-1.8/spock-report

####Generate a HTML report based on labels

Project on github - https://github.com/chanwit/spock-report-html

###Extensions

####Useful Extensions

  • @Unroll - splits out features into separate tests
  • @Ignore - do not run this test
  • @Ignore(reason = "I am too old for this shit") - can have a reason
  • @IgnoreRest - only run this test
  • @IgnoreIf({ os == 'windows' })
  • @Require({ payscale == 'jedi' })
  • @Stepwise - causes each feature method to be executed in declaration order and any after the first failure to be ignored (i.e. story mode)
  • @Shared - makes a field available to all tests / where clauses
  • @Timeout - causes a method to fail if it takes to long (and will interrupt it if it does take too long)
  • @AutoCleanup - allows a named cleaning up/closing method to be automatically called by Spock on a field
  • @FailsWith - makes a feature pass only if it raises a certain exception and it’s uncaught

Annotations like @Unroll and @Timeout can be applied to the spec level.

####Make your own extensions

Follow Luke Daley's guide at http://ldaley.com/post/971946675/annotation-driven-extensions-with-spock

####Hamcrest Matchers

import static spock.util.matcher.HamcrestSupport.that

expect:
that answer, closeTo(42, 0.001)

You can also use hamcrest matchers to constraint method arguments

import static spock.util.matcher.HamcrestMatchers.closeTo

1 * foo.bar(closeTo(42, 0.001))

In then methods, you can use 'expect' for better readability

when:
def x = computeValue()

then:
expect x, closeTo(42, 0.01)

Useful list of Hamcrest Matchers - https://code.google.com/p/hamcrest/wiki/Tutorial

####Parameter Typing You can add type information to the method (for IDE support).

def "some math"(Integer a, Integer b, Integer c) {
 expect:
 a + b == c
 where:...
}

###Working with Data - Data Tables

####One column where clauses

where:
  animal   | _
  'rabbit' | _
  'cow'    | _

####Double Pipe to separate input from output

where:
  english  | plural || spanish 
  'rabbit' | true   || 'conejos'
  'cow'    | false  || 'vaca'

####Assign variables

where:
   a = 'cars'

####Data Pipes

where:
   a << ['one', 'two', 'three' ]

This will run each value of the pipe at each iteration - resulting in three tests.

####Multiple assignments

where:
   [a, b, c] << [ [ 1, 2, 3 ],
                  [ 1, 3, 5 ] ]

####Skip values

where:
   [a, _, c] << [ [ 1, 2, 3 ], [ 1, 3, 5 ] ]

####Combinations

Creates a data table with all 8 possible combinations:

where:
   [name, fingers, nombre] << [ [ 'one', 'two'], [1,2], ['uno', 'dos'] ].combinations()

This is equivalent to:

where:
   name  | fingers | nombre
   'one' |    1    | 'uno'
   'two' |    1    | 'uno'
   'one' |    2    | 'uno'
   'two' |    2    | 'uno'
   'one' |    1    | 'dos'
   'two' |    1    | 'dos'
   'one' |    2    | 'dos'
   'two' |    2    | 'dos'

####Can combine different types of data table format

where:
   name  | nombre
   'one' | 'uno'

   properName = name.capitalize()
   postCode << ['90210', '90222']

IntelliJ won't recognize mixed format so you have to do the data table, format it and then the other values.

####Parameter Typing You can add type information to the method (for IDE support).

def "some math"(Integer a, Integer b, Integer c) {
    expect:
    a + b == c

    where:
    a | b || c
    1 | 1 || 2
    5 | 5 || 10
}

###Mocking

####Declarations can be chained

foo.bar() >> { throw new IOException() } >>> [1, 2, 3] >> { throw new RuntimeException() }

####You can group Interactions together with the 'with' keyword

def service = Mock(Service)
app.service = service

when:
app.run()

then:
with(service) {
    1 * start()
    1 * act()
    1 * stop()
}

####Groovy mocks

If you have dynamic methods, use the groovy version of mocks and stubs,

def mock = GroovyMock(Person)
def stub = GroovyStub(Person)
def spy = GroovySpy(Person)

They can also be made global

def spy = GroovySpy(Person, global:true)

####Testing Domain Class Constraints with Spock

http://www.christianoestreich.com/2012/11/domain-constraints-grails-spock-updated/

####Presentations on Spock

##Betamax

####Presentations on Betamax

##Geb

####Book of Geb http://www.gebish.org/manual/current/

####Adding a button that pauses Geb via javascript (Luke Daley)

   private void pause() {
       js.exec """
           (function() {
               window.__gebPaused = true;
               var div = document.createElement("div");
               div.setAttribute('style', "\\
                   position: absolute; top: 0px;\\
                   z-index: 3000;\\
                   padding: 10px;\\
                   background-color: red;\\
                   border: 2px solid black;\\
                   border-radius: 2px;\\
                   text-align: center;\\
               ");

               var button = document.createElement("button");
               button.innerHTML = "Unpause Geb";
               button.onclick = function() {
                   window.__gebPaused = false;
               }
               button.setAttribute("style", "\\
                   font-weight: bold;\\
               ");

               div.appendChild(button);
               document.getElementsByTagName("body")[0].appendChild(div);
           })();
       """

       waitFor(300) { !js.__gebPaused }
   }

####Extend your own elements by providing your own navigators (Sky)

Provide your own class for empty and nonempty navigators

class NonEmptyNavigator extends geb.navigator.NonEmptyNavigator {
    NonEmptyNavigator(Browser browser, Collection<? extends WebElement> contextElements) {
        super(browser, contextElements)
    }

    boolean isDirty() {
        hasClass 'dirty'
    }

    boolean isHidden() {
        hasClass 'hidden'
    }
}

Add this to GebConfig

innerNavigatorFactory = { Browser browser, List<WebElement> elements ->
    elements ? new NonEmptyNavigator(browser, elements) : new EmptyNavigator(browser)
}

####Getting Raw HTML for an element

in your custom Navigator

 String rawHtml() {
        browser.js.exec(firstElement(), "return arguments[0].innerHTML;")
    }

####Handling css transitions (Sky)

Add this to the nonempty navigator / webkit only

void waitForCssTransition(Closure trigger) {
        def element = firstElement()

        browser.js.exec(element, '''
            var o = jQuery(arguments[0]);
            window.setTransitionFinishedClass = function() {
                $(this).addClass('transitionFinished');
            }
            o.bind('webkitTransitionEnd', window.setTransitionFinishedClass);
        ''')

        try {
            trigger.call()
            browser.waitFor {
                hasClass('transitionFinished')
            }
        } finally {
            browser.js.exec(element, '''
                var o = jQuery(arguments[0]);
                o.removeClass('transitionFinished')
                o.unbind('webkitTransitionEnd', window.setTransitionFinishedClass);
                window.setTransitionFinishedClass = undefined;
            ''')
        }
}

####Automatically download chromedriver (Sky)

Into your gebConfig

driver = { new FirefoxDriver() }

private void downloadDriver(File file, String path) {
    if (!file.exists()) {
        def ant = new AntBuilder()
        ant.get(src: path, dest: 'driver.zip')
        ant.unzip(src: 'driver.zip', dest: file.parent)
        ant.delete(file: 'driver.zip')
        ant.chmod(file: file, perm: '700')
    }
}

environments {

    // run as “grails -Dgeb.env=chrome test-app”
    // See: http://code.google.com/p/selenium/wiki/ChromeDriver
    chrome {
        def chromeDriver = new File('test/drivers/chrome/chromedriver')
        downloadDriver(chromeDriver, "http://chromedriver.googlecode.com/files/chromedriver_mac_23.0.1240.0.zip")
        System.setProperty('webdriver.chrome.driver', chromeDriver.absolutePath)
        driver = { new ChromeDriver() }
    }

####Page Builder Pattern (Craig Atkinson)

Helps make tests more explicit by having helper methods always return the actual page.

Instead of

to HomePage 
loginButton.click()

make it

Homepage homepage = to(HomePage)
homepage.loginButton.click()

all helper methods should return the page as the last argument

SignedInPage login( String username, String password ){
      // code here
      at SignedInPage
      browser.page
}

In your tests, you call

when:
  Homepage homepage = to(HomePage)
  SignedInPage signedInPage = login( 'bob', 'password' )
then:
  signedInPage.displayedUsername == 'Bob Marley'

####Inject a js library to the page

Add helper method to Geb base page

def injectLibrary( library ){
     js.exec("document.body.appendChild(document.createElement('script')).src='$library'"); 
}

Call in page that needs injection

injectLibrary( 'http://sinonjs.org/releases/sinon-1.4.2.js' )

####Setting up headless browsing on Jenkins with xvfb

http://www.labelmedia.co.uk/blog/setting-up-selenium-server-on-a-headless-jenkins-ci-build-machine.html

####Use Sauce Labs

add dependency to buildconfig

test "org.seleniumhq.selenium:selenium-remote-driver:2.31.0"

in GebConfig

import org.openqa.selenium.remote.DesiredCapabilities
import org.openqa.selenium.remote.RemoteWebDriver
driver = {
   DesiredCapabilities capabilities = DesiredCapabilities.firefox()
   capabilities.setCapability("version", "17")
   capabilities.setCapability("platform", "Windows 2012")
   new RemoteWebDriver(
     new URL("http://:<access_key>@ondemand.saucelabs.com:80/wd/hub"), capabilities
   )
}

####Use PhantomJS via Ghostdriver

In BuildConfig

test( "com.github.detro.ghostdriver:phantomjsdriver:1.0.1" ) {
   transitive = false
}

in GebConfig

import org.openqa.selenium.phantomjs.PhantomJSDriver
import org.openqa.selenium.remote.DesiredCapabilities
import org.openqa.selenium.Dimension
driver = {
    new PhantomJSDriver(new DesiredCapabilities())
}

####Switching to a different driver for one Spec

import geb.spock.GebSpec
import spock.lang.Shared
import org.openqa.selenium.firefox.FirefoxProfile
import org.openqa.selenium.firefox.FirefoxDriver
 
class MobileSpeck extends GebSpec {
 
@Shared def cachedDriver
 
def setupSpec(){
  cachedDriver = new FirefoxDriver()
}
 
def setup(){
  // assign this as the default driver on the browser for each test
  browser.driver = cachedDriver
}
 
def cleanupSpec(){
  // after running the spec, kill the driver
  cachedDriver.quit()
}
 
}

####Grails polydriver plugin (Object Partners)

Set up multiple drivers and define preferred driver for one spec http://www.objectpartners.com/2013/05/05/poly-driver-a-phantom-band-aid-for-geb/

####Run Javascript tests in Sauce Labs via Geb (Object Partners)

http://www.objectpartners.com/2013/04/18/multi-browser-javascript-unit-testing-with-sauce/

####Changing window sizes for tests (Marcin Erdmann) http://blog.proxerd.pl/article/resizing-browser-window-in-geb-tests

####Manipulating time with Sinon http://blog.proxerd.pl/article/mocking-browser-timers-in-geb-tests

####Modelling repeated structures with Geb http://adhockery.blogspot.co.uk/2010/11/modelling-repeating-structures-with-geb.html

Update: Also use ModuleList - http://www.gebish.org/manual/0.9.0/modules.html#using_modules_for_repeating_content_on_a_page

####Useful WebDriver settings

Fixed Screen Size

import org.openqa.selenium.phantomjs.PhantomJSDriver
import org.openqa.selenium.remote.DesiredCapabilities
import org.openqa.selenium.Dimension
driver = {
    def d = new PhantomJSDriver(new DesiredCapabilities())
    d.manage().window().setSize(new Dimension(1028, 768))
    d
}

Spoof User Agent in PhantomJS

def capabilities = new DesiredCapabilities()
capabilities.setCapability("phantomjs.page.settings.userAgent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/27.0.1453.93 Safari/537.36")
new PhantomJSDriver(capabilities)

Spoof User Agent Firefox

FirefoxProfile profile = new FirefoxProfile();
profile.setPreference("general.useragent.override", 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.1');
cachedDriver = new FirefoxDriver(profile)

Accessing SSL Certificates

http://onekilo79.wordpress.com/2013/08/17/groovy-with-geb-using-phantomjs-and-ssl-certs/

####Hybrid model: add Sikuli to geb tests for things Geb can't deal with like File upload dialogs or system interactions.

http://fbflex.wordpress.com/2012/10/27/geb-and-sikuli/

####Presentations on Geb

About

My collection of Grails testing related snippets

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published