May 172012
 

I found my self in a situation where I was writing shell scripts, something I usually don’t do. I tried a test driven approach to improve the quality and document the functionality, and it turned out to work very well.

Background

I’m working in a project where we are migrating a system from JBoss 5 to 7. As a part of this work we are throwing out seam, changing the logging framework and some other minor changes. The result is that we have a lot of code where we need to make repeated modifications to, such as removing or replacing annotations, updating loggers and so on. This is boring work so we decided to automate as much as possible with shell scripts.

We started hacking away, none of us being a shell script expert, writing scripts mostly doing search and replace with regex. Soon it became messy and hard to understand so we needed a better approach. I usually do most of my work in java and work test driven, so why not try that on shell scripting?

Solution

I extracted the script parts doing the actual transformation work into small testable functions that were placed in one file, convertUtil.sh. Another file, convertUtilTest.sh, was created with the unit tests. The approach I used to test the functions was to generate an input file, apply the function under test to the generated file and finally to compare it with the expected result. The comparison is done with diff, so if the result is not what we expect diff will display that, otherwise it will be silent. I added a function that is run before each test to display the name of the test and a function to run all tests.Now I could simply run convertUtilTest.sh and if I get a list of all test cases without any diffs I know my tests were passing.

This is what the code looked like.
convertUtil.sh

# Remove all @Local annotations without arguments
# $1 The root dir to convert
function removeLocalAnnotations {
	find $1 -name "*.java" -print | xargs perl -0777 -p -i -e "s/\@Local\s*?(\n)/\1/sg"
}

# Find an @EJB annotation and add a beanName
# $1 = the file name
# $2 = the name of the attribute that is annotated
# $3 = the bean name to use in the annotation
function addBeanName {
	perl -0777 -p -i -e "s/(\@EJB)([^\n]*?\n[^\n]*?$2\s*?;)/\1(beanName=\"$3\")\2/sg" "$1"
}

# ... more functions doing various conversion

convertUtilTest.sh

source convertUtil.sh

function beforeEachTest {
	testarea="/tmp/testarea"
	input="$testarea/input.java"
	expected="$testarea/expected.java"
}

# ---------------------------------------------------------------------------------------

function removeLocalAnnotationsTest {
	cat > "$input" << EOF
	@Local
	@Local(withArguments)
	public void local()
	class Local {
EOF

	removeLocalAnnotations "$testarea"

	cat > "$expected" << EOF

	@Local(withArguments)
	public void local()
	class Local {
EOF

	diff $expected $input
}

# ---------------------------------------------------------------------------------------

function addBeanNameTest {
	cat > "$input" << EOF
	@EJB
	SomeClass<SomeOtherClass> myMember;
	@EJB
	SomeClass<SomeOtherClass> myOtherMember;
EOF

	addBeanName "$input" "myMember" "MyImpl"

	cat > "$expected" << EOF
	@EJB(beanName="MyImpl")
	SomeClass<SomeOtherClass> myMember;
	@EJB
	SomeClass<SomeOtherClass> myOtherMember;
EOF

	diff $expected $input
}

# ---------------------------------------------------------------------------------------

# ... more unit tests

# ---------------------------------------------------------------------------------------

# Execute one test
# $1 the test
function oneTest {
	beforeEachTest
	echo $1
	$1
}

function allTests {
	echo "--- Start of tests"
	oneTest addBeanNameTest
	oneTest removeLocalAnnotationsTest
	# ... more calls to unit tests
	echo "--- End of tests"
}

# Run the tests
allTests

The output when all tests pass looks like this

Kristoffer-Richardssons-MacBook-Air:cbe kristofferrichardsson$ ./convertUtilTest.sh
--- Start of tests
addBeanNameTest
removeLocalAnnotationsTest
--- End of tests

A failing test (where we have an error in addBeanName) would look like this

Kristoffer-Richardssons-MacBook-Air:cbe kristofferrichardsson$ ./convertUtilTest.sh
--- Start of tests
addBeanNameTest
1c1
< @EJB(beanName="MyImpl")
---
> @EJB(beanNaem="MyImpl")
removeLocalAnnotationsTest
--- End of tests

If you only want to run a single test, it is easy to simply change the bottom line in the test file to

# Run the tests
# allTests
oneTest addBeanNameTest

After this I could start doing some real work by writing the test first and the script after that.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>