Groovy for testers Apache Groovy

Groovy for testers


After initially shunning the language, I have used Groovy since late 2015. Now in the stable of the Apache Foundation, Groovy is such a pleasure to code in. For me it is like a happy intersection of Ruby, Python an Java. I have used all three those, and other languages extensively in test automation, but Groovy's syntactic sugar makes it effortless to do tasks that are more cumbersome in other languages, especially Java. Yet it has the power and the ease of library incorporation that Java has, or better. Ever tried Ruby gems or Python pip from behind a draconian corporate NTLM proxy? It sucks.

Don't expect Groovy code in the distinguished halls of computer science (not that it stands back for any other JVM language). Instead, you'll often it in the kitchen, happily doing the dirty work. And doing it well. To me, Groovy has become one of the de-facto programming languages for software test automation. It has had Geb and Spock for long, it is the built-in scripting language in SoapUI and of late in JMeter. It plays very well with Selenium, Cucumber and Gherkin. And it is just a good language for the fetch-and-carry sort of forklift work that is required for back-end testing. Interact with queues, databases, filesystems and so on with less effort than Java or Python (IMHO).

As a side note, the name Groovy more than anything else, made me shy away from the language initially. Just the other day I did a one-slide system metaphor for a test automation solution, and I was wondering what the executives think when they see Groovy in the core of the metaphor. But now I understand the name. It suits my I'm-more-chilled-because-I'm-over-forty outlook.

Now that I have lowered your expectations, here are some out of context snippets that I have found remarkable or useful, which is what this whole site is really about. Or maybe it's just a fan page. I don't care. Also, I'm not going to discuss the best IDE's, how to install the language, where to get libraries and tools, or how to print hello world - you can find that in plenty of other places. The Groovy logo at the top is the only real external link on this page. Click it. I may add more pages focused on other tools in future. For now, I also have a page about SoapUI and a page about VBScript for testers.

The four areas where Groovy really works for me every day, are: string manipulation, collection iteration, database interaction and handling xml. here are some examples of those and others.

Kick-off
String manipulation
Collection iteration
Random numbers and strings
Filesystem interaction
Handling xml
Database work
Graphing with charts
Bonus level (run Python code from Groovy)

Kick-off

This is a way to get your important system variables. For example, you are sitting in front of a PC with SoapUI installed, and want an inventory of what you have. Unfortunately the version of Groovy that ships with SoapUI is normally quite old.
println "os.arch: " + System.getProperty("os.arch")
println "java -version".execute().err.text
println "GroovySystem.version: " + GroovySystem.version
println "java.library.path: " + System.properties["java.library.path"]

String manipulation

Test automation is all about pattern matching, replacing values and other ways of manipulating data that is inputted or emitted from systems. For the uninitiated, using assert like this in examples means it is running code, but you can also see the output without actually running it. Neat.
assert 'Groovy'.length() == 6
assert 'Groovy'.substring(3) == 'ovy'
assert 'Groovy'.substring(0,2) == 'Gr'
assert 'Groovy'.take(4) == 'Groo'

assert 'Groovy'.replace('oo', 'a') == 'Gravy' 
assert 'Groovy'.take(3).replace('o', 'u') == 'Gru'
assert 'Groovy'.replaceAll('o', 'a') == 'Graavy'

assert 'Groovy'.toLowerCase() == 'groovy'
assert 'Groovy'.toUpperCase() == 'GROOVY'
assert 'groovy'.capitalize() == 'Groovy'
assert 'grOOvy'.capitalize() == 'GrOOvy'
assert 'grOOvy'.toLowerCase().capitalize() == 'Groovy'
assert 'Groovy'.reverse() == 'yvoorG'

assert 'Groovy'.contains('oo') == true
assert 'Groovy'.equals('Groovy') == true
assert 'Groovy'.equalsIgnoreCase('gRoOvY') == true
assert 'Groovy'.startsWith('Gr')
assert 'Groovy'.endsWith('vy')
assert 'Groovy'.isEmpty() == false
assert 'Groovy'.indexOf('v') == 4

assert 'Groovy'.matches(/^Gr.*/) == true
assert 'Groovy'.matches(/.*oo.*/) == true
assert 'Groovy'.matches(/.*vy$/) == true
assert 'Groovy'.split('r') == ['G', 'oovy']

assert 'Groovy'.padLeft(10, '*') == '****Groovy'
assert 'Groovy'.padRight(10, '*') == 'Groovy****'
assert 'Groovy'.center(10, '*') == '**Groovy**'

assert 'Groovy' - 'o' == 'Grovy'
assert 'Groovy' + 'Baby' == 'GroovyBaby'
assert 'Groovy' * 2 == 'GroovyGroovy'
Replace part of a string based on a pattern match:
assert '05.00'.replaceAll(/^0/, '') == '5.00'
Here we have a list, but it is derived from a string:
assert 'I love Groovy'.tokenize() == ['I', 'love', 'Groovy']

Collection iteration

Ranges with closures, similar to a block in Ruby
1.upto(4) { println "Number ${it}" }

3.times { println "Hello World ${it}" }

'Groovy'.collect { println 'letter:' + it  }

(1..10).collect { println it * 2 }

(1..10).each { println ((it % 5 == 0) ? "${it} is a factor of 5" : "${it} is not a factor of 5") }

('a'..'f').eachWithIndex { it, index ->
	println "${it} is at position ${index + 1}"
}
sum() works a little differently:
def range = (1..1000) as Integer[]
assert range.sum() == 500500
One line FizzbBuzz with the Elvis operator:
(1..100).each { println ( (it % 3  ? "" : "Fizz") + ( it % 5 ? "" : "Buzz") ?: it ) }

Lists:
li = [14, 12, 35, 22, -5, 46, 98, 53, 29, 87, 44]
println li.getClass()

assert li[0] == 14
assert li[-1] == 44
assert li.getAt(1) == 12

assert li.size() == 11
assert li.min() == -5
assert li.max() == 98
assert li.sum() == 435
Let's make our own average method using Runtime Metaprogramming:
List.metaClass.avg = {
	return delegate.size() ? delegate.sum() / delegate.size() : 0
}
assert li.avg() == 39.5454545455
Cool. Continuing with lists:
assert li.join("|") == '14|12|35|22|-5|46|98|53|29|87|44'

assert li.find { it > 97 } == 98
assert li.find { it.toString() =~ /^4.*/ } == 46
assert li.findAll { it.toString() =~ /^4.*/ } == [46, 44]
assert (li.contains(35)) ? 'yes' : 'no' == 'no'

assert li.findAll { it < 15 } == [14, 12, -5]
assert li.findIndexOf{ it == 12 } == 1
assert li.contains(13) == false
assert li.isEmpty() == false

li2 = li
li2.addAll([7, 32])
assert li2 == [14, 12, 35, 22, -5, 46, 98, 53, 29, 87, 44, 7, 32]
li2.removeElement(-5)
assert li2.sort() == [7, 12, 14, 22, 29, 32, 35, 44, 46, 53, 87, 98]
assert li2.reverse() == [98, 87, 53, 46, 44, 35, 32, 29, 22, 14, 12, 7]

Filter:
def (passed, failed) = [39, 56, 67, 76, 82, 89, 91].split{ it > 60 }
// or
println ([39, 56, 67, 76, 82, 89, 91].groupBy { it > 60 ? true : false })

Maps:
m = [sky:'blue', grass:'green', ocean:'blue', ground:'brown', sun:'yellow']

assert m.get('ocean') == 'blue'
assert m.size() == 5

m.put('night', 'black')
assert m['night'] == 'black'
assert m.size() == 6

println m[]
m.remove('sun')
assert m.size() == 5

m.each { key, value -> if (key.startsWith("gr")) println value }

def found = m.find{ it.key == "sky" }?.value
assert found.equals("blue")

Reverse sort a map:
map = ['02.00':2, '06.00':6, '01.00':1, '04.00':4, '05.00':5, '03.00':3]
println map.sort { -it.value }

Random numbers and strings

The ability to create random numbers or strings is often handy for quickly generating test data. Here we define four methods that provide four different types of random data.
def randStr() { //random size
	return new Random().with { (1..(Math.abs(new Random().nextInt() % 5) + 1))
				    .collect { (('A'..'Z')+('0'..'9')+('a'..'z'))
				    .join()[ nextInt( (('A'..'Z')+('0'..'9')+('a'..'z'))
				    .join().length() ) ] }
				    .join() }
}
	
def randNum() { //random size
	return Math.abs(new Random().nextInt() % (Math.abs(new Random().nextInt()))) + 1
}

def randStr(desiredSize) { 
	return new Random().with { (1..desiredSize)
				    .collect { (('A'..'Z')+('0'..'9')+('a'..'z'))
				    .join()[ nextInt( (('A'..'Z')+('0'..'9')+('a'..'z'))
				    .join().length() ) ] }
				    .join() }
}
	
def randNum(maxSize) {
	return Math.abs(new Random().nextInt() % maxSize) + 1 
}

println "random string: ${randStr()}"
println "random number: ${randNum()}"
println "random string of 10 characters: ${randStr(10)}"
println "random number between 1 and 3: ${randNum(3)}"
println "random number between 1 and a million: ${randNum(1000000)}"

Filesystem interaction

Files are easy to work with
def myFile = new File("./src/myfile.txt")
myFile.write("Hello from Groovy\n")  //overwrites existing file
myFile << "More text\n" // appends
myFile.append("Last line")

println myFile.text.getClass()
println myFile.text
println myFile.text.size()

println myFile.readLines().getClass()
println myFile.readLines().size()
Use date and time in a filename:
import java.text.SimpleDateFormat
testDate = new SimpleDateFormat('yyyy-MM-dd HH-mm').format(new Date())
def logFile = new File("./src/log-${testDate}.txt")
A note on comparing and formatting dates:
Date date = Date.parse( 'dd-MMM-yyyy', '07-OCT-2018' )
newDate = date.format( 'd-M-yyyy' ) 
assert newDate == '7-10-2018'
Get last modified date and time of files:

new File('.').eachFileRecurse { file ->
	println new Date(file.lastModified()).format('EEE MMM dd hh:mm:ss a yyyy')
}
Recursive search:
import static groovy.io.FileType.FILES
List li = []
new File('.').eachFileRecurse(FILES) { f -> 
    if(f.name.endsWith('.groovy')) {
	println "file name ${f} found" 
	f.readLines().each { l ->
	    if (l.startsWith("import")) {
		println "found import: ${l}"
		li.add(l)
	    }
	}
    }
}

li.sort().unique().each { i ->
	println i
}
AntBuilder - handy for manipulating files and directories

A side note about @Grab: This annotation automatcally uses Ivy / Maven to get the jars you require, and their dependencies.
It can be a bit of a dark art to set up, but once it works, it works well. In theory, on a new install of Groovy, without a proxy,
this *should* work out of the box. I have it here for reference to the libs that are required, more than as a working example. YMMV.

@Grab(group='org.apache.ant', module='ant', version='1.9.8')
@Grab(group='org.apache.ant', module='ant-launcher', version='1.9.8')

def ant = new AntBuilder()

def file = new File(ant.project.baseDir,"src/myfile.txt")
assert file.exists()

ant.replace(file: file, token: ", );", value: ");")

ant.mkdir(dir: "../backup")

ant.copy( todir:"../backup" ) {
	fileset( dir:"./src/" )
  }
  
def zipfile = '../backup/backup.zip'
def current = './src'
ant.zip(destfile: zipfile) {
    fileset(dir: current) {
        include(name: '**/*.*')
    }
}

ant.delete(dir:'../backup',failonerror:false)

Handling xml

The simplicity of Groovy's standard library for XML handling is poetic.

HTML's handling of xml - not so poetic, if the display of this XML breaks in your browser, I'm sorry... here it is in a file

def myXmlString = '''
<transaction> <payment> <txID>68246894</txID> <customerName>Huey</customerName> <accountNo type="Current">15778047</accountNo> <txAmount>899</txAmount> </payment> <receipt> <txID>68246895</txID> <customerName>Dewey</customerName> <accountNo type="Current">16288</accountNo> <txAmount>120</txAmount> </receipt> <payment> <txID>68246896</txID> <customerName>Louie</customerName> <accountNo type="Savings">89257067</accountNo> <txAmount>210</txAmount> </payment> <payment> <txID>68246897</txID> <customerName>Dewey</customerName> <accountNo type="Cheque">123321</accountNo> <txAmount>500</txAmount> </payment>
'''
Note how the XML handle and gpath in your code is in line with the XML structure and xpath. The root node becomes your variable for the XML string.
def transaction = new XmlSlurper().parseText(myXmlString)
Get some values from nodes and elements:
assert transaction.payment.txAmount[0] == '899'
assert transaction.payment.accountNo.@type[1] == 'Savings'
assert transaction.payment[2].accountNo.@type == 'Cheque'
assert transaction.receipt[0].txID == '68246895'
Find values based on criteria
transaction.payment.findAll { tx ->
	tx.txAmount.toInteger() > 300
}.each { tx ->
	println "${tx.customerName} made payment of ${tx.txAmount} with transaction id ${tx.txID}."
}
Update a node or element's value in the XML
transaction.payment[1].customerName = 'Bob'
def stringWriter = new StringWriter()
println groovy.xml.XmlUtil.serialize(transaction)
Replace a node
transaction.receipt[0].replaceNode{
	receipt(receiptNo:"65413"){
		   receiptdate("2018-09-30")
		   customerName("Dewey")
		   accountType("Current")
		   accountNo("16288")
		   txAmount("120") }
}
println groovy.xml.XmlUtil.serialize( transaction )
Add a node
def newTransaction = new XmlParser( false, true ).parseText( myXmlString )
newNode = "		2018-09-30"
fragmentToAdd = new XmlParser( false, true ).parseText( newNode )
newTransaction.'**'.find { it.name() == 'receipt' }.children().add( 2, fragmentToAdd )
println groovy.xml.XmlUtil.serialize( newTransaction )
Remove a node
newTransaction.NameValuePairs.NameValuePair.findAll { it.name.text() == 'accountNo' }*.replaceNode{}
println groovy.xml.XmlUtil.serialize( newTransaction )

Database work

Working with a database in groovy is so straightforward, I am not even going to say much about it:
@Grab(group='mysql', module='mysql-connector-java', version='5.1.32')

import groovy.sql.Sql
def myDB = Sql.newInstance('jdbc:mysql://127.0.0.7:3306/employees', 'user1', 'pass1', 'com.mysql.jdbc.Driver')
Selecting one row:
def numberOfRecords = myDB.firstRow("select count(*) as count from employees.employees;")
println "There are ${numberOfRecords.count} in the employees table"
Rowset as iterable list:
myDB.eachRow("select first_name, last_name from employees.employees;"){
	println "${it.first_name} ${it.last_name}"
}
Changing data:
myDB.execute("update employees.employees
                set last_name = 'Bradford'
                where last_lame = 'Simmel'
                and first_name = 'Bezalel';")

myDB.executeInsert("INSERT INTO `employees` VALUES 
                    (10000,'1960-10-01','Peter','Morgan','M','1995-02-10');")
Remember to tidy up:
myDB.close()
Make a method that inserts for you:

The ? parameters is for JDBC to optimise the query by precompiling it with parameters. Always use it.
If you use "${}" gstrings in your query for variables, the SQL is not optimised by JDBC, and it will carp about it.

def insertEmployee(emp_no, birth_date, first_name, last_name, gender, hire_date) {
	def db = Sql.newInstance('jdbc:mysql://127.0.0.7:3306/employees', 
                            'user1', 'pass1', 'com.mysql.jdbc.Driver')
	
	db.executeInsert("insert into employees VALUES (?, ?, ?, ?, ?, ?);",
                            [emp_no, birth_date, first_name, last_name, gender, hire_date])
	db.close()
}
Connect to MS-SQL Server with domain authentication:
Sql.newInstance("jdbc:jtds:sqlserver://SERVERNAME:1433/dbname;
                    useLOBs=false;
                    instance=SERVERNAME;
                    autoCommit=true;
                    useNTLMv2=true;
                    domain=MYDOMAIN",
                    "MyUser", "MyPassword", "net.sourceforge.jtds.jdbc.Driver")

Graphing with charts

For long I looked for a library to use for making graphs. I finally found one that works well with Groovy, and that is small in lines of code you have to write.

Bar chart:
@Grab(group='org.knowm.xchart', module='xchart-parent', version='3.5.2', type='pom')
@Grab(group='org.knowm.xchart', module='xchart', version='3.5.2')

import org.knowm.xchart.CategoryChart
import org.knowm.xchart.CategoryChartBuilder
import org.knowm.xchart.style.Styler.LegendPosition
import org.knowm.xchart.BitmapEncoder
import org.knowm.xchart.BitmapEncoder.BitmapFormat

CategoryChart chart = new CategoryChartBuilder().width(800)
                                                .height(600)
                                                .title("Score Histogram")
                                                .xAxisTitle("Score")
                                                .yAxisTitle("Number")
                                                .build()

chart.getStyler().setLegendPosition(LegendPosition.InsideNW)
chart.getStyler().setHasAnnotations(true)

chart.addSeries("test 1", [ 0, 1, 2, 3, 4 ], [ 4, 5, 9, 6, 5 ])

new SwingWrapper(chart).displayChart()
BitmapEncoder.saveBitmap(chart, "./src/Bar.png", BitmapFormat.PNG)
Pie chart:
@Grab(group='org.knowm.xchart', module='xchart-parent', version='3.5.2', type='pom')
@Grab(group='org.knowm.xchart', module='xchart', version='3.5.2')

import java.awt.Color
import org.knowm.xchart.PieChart
import org.knowm.xchart.PieChartBuilder
import org.knowm.xchart.internal.chartpart.Chart
import org.knowm.xchart.BitmapEncoder
import org.knowm.xchart.BitmapEncoder.BitmapFormat

PieChart chart1 = new PieChartBuilder().width(800)
                                      .height(600)
                                      .title(getClass().getSimpleName())
                                      .build()

Color[]  sliceColors = [ new Color(224, 68, 14),
                         new Color(230, 105, 62),
                         new Color(236, 143, 110),
                         new Color(243, 180, 159),
                         new Color(246, 199, 182) ]

chart1.getStyler().setSeriesColors(sliceColors)

chart1.addSeries("Gold", 24)
chart1.addSeries("Silver", 21)
chart1.addSeries("Platinum", 39)
chart1.addSeries("Copper", 17)
chart1.addSeries("Zinc", 40)
   
new SwingWrapper(chart1).displayChart()
BitmapEncoder.saveBitmap(chart1, "./src/Pie.png", BitmapFormat.PNG)
Make a wordcloud with a different library
@Grab(group='com.kennycason', module='kumo', version='1.17', type='pom')
@Grab(group='com.kennycason', module='kumo-core', version='1.17')
@Grab(group='com.kennycason', module='kumo-api', version='1.17')

import static groovy.io.FileType.FILES
import com.kennycason.kumo.CollisionMode
import com.kennycason.kumo.WordFrequency
import com.kennycason.kumo.WordCloud
import com.kennycason.kumo.bg.RectangleBackground
import com.kennycason.kumo.font.scale.LinearFontScalar
import com.kennycason.kumo.nlp.FrequencyAnalyzer
import java.awt.Dimension
import java.awt.Color


ofile = new File("./src/words.txt")
ofile.write('')

["./src"].each { dir ->
	 findWords(dir, ofile)
 }

def findWords(dir, ofile) {
    new File(dir).eachFileRecurse(FILES) {
	if(it.name.endsWith('.groovy')) {
            println it.name
            it.readLines().each { l ->
                if (! l.contains("//")) {
                    w = l.split()
                    w.each() { word ->
			if (! [ "//", "\\", '=', '==', '{', '}', '[', ']', '->',
                                '|', '>=', '<=', '>', '<', '+', '-'].contains(word)) {
                            println word
			    ofile.append("${word}\n")
                        }
                    }
		}
	    }
	}
    }
}

FrequencyAnalyzer frequencyAnalyzer = new FrequencyAnalyzer()
List wordFrequencies = frequencyAnalyzer.load("./src/words.txt")
Dimension dimension = new Dimension(600, 600)
WordCloud wordCloud = new WordCloud(dimension, CollisionMode.RECTANGLE)
wordCloud.setPadding(0)
wordCloud.setBackground(new RectangleBackground(dimension))
wordCloud.setFontScalar(new LinearFontScalar(10, 40))
wordCloud.build(wordFrequencies)
wordCloud.writeToFile("./src/wordcloud.png")

Bonus level

I once needed to execute Python code from within Groovy. Don't ask. I used Jython.
@Grab(group='org.python', module='jython-standalone', version='2.7.1b3')

import org.python.util.PythonInterpreter
import org.python.core.*

class AJythonCall {
    static void callJython() throws PyException {
	
	PythonInterpreter jython = new PythonInterpreter()

	println "Hello, Jython"
	jython.exec("import sys")
	jython.exec("print sys")

	jython.set("a", new PyInteger(42))
	jython.exec("print a")
		
	jython.exec("x = 2 + 2")
	PyObject x = jython.get("x")

	println "x: " + x
	println "Bye, Jython"
    }
}

class AJythonScriptCall {
    static void callJythonScript() throws PyException {
		
	PythonInterpreter jython = new PythonInterpreter()
		
	jython.exec("""
		    |num = 1
		    |if num >= 0:
		    |    if num == 0:
		    |        print("Zero")
		    |    else:
		    |        print("Positive number")
		    |else:
		    |    print("Negative number")
		    """.stripMargin())
	}
}

a = new AJythonCall().callJython()
b = new AJythonScriptCall().callJythonScript()

© 2003-2018 ou-ryperd.net