Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam ( @bittersweetryan )

Learning About Test-Driven Development (TDD) Using Tiny Test

By on
Tags:

Unit testing and Test-Driven Development (TDD) have, collectively, been one of the largest blind spots in my view of the programming world. A couple of weeks ago, in an attempt to remedy this, I created a very small unit testing framework - Tiny Test - such that I could learn about unit testing from the inside-out. Building the Tiny Test framework helped me understand how unit tests work; but, I wanted to field test some TDD using actual code. As such, I created a "dummy" project, QueryHelper.cfc, that I would build using Tiny Test.

View this project on GitHub.

According to Robert Martin (aka. Uncle Bob), there are three fundamental rules to writing unit tests:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

And so, this is how I built QueryHelper.cfc. To be honest, I didn't really even know what behaviors I wanted to put in the Query Helper component - this was more of a TDD exploration than an actual project. So, I started out with just getting it to initialize. Then, I started to add (and subsequently fix) one failing unit test at a time.

One thing that I really love about Tiny Test, as I explore Test-Driven Development, is that I [basically] drop it in and it just works. No setting up any programmatic test harnesses. No telling it where to find test cases. I changed one "lib" mapping and everything took care of itself. And, the ALT-TAB / auto-running of tests is just pleasing to the touch.

While I like keeping the collection of "assert" methods very small, I can see that it would be very useful to have complex-object comparison functions. I think I might add those to the "TestCase" that the developer is allowed to modify; I'd rather have them as an extension of the Tiny Test framework than as part of the core feature set.

Anyway, after some Red-Green-Refactor, here's my current test case:

component
	extends = "TestCase"
	output = false
	hint = "I test the QueryHelper component."
	{


	// ---
	// LIFECYCLE METHODS.
	// ---


	// I get called before each test method.
	public void function setup() {

		queryHelper = new lib.QueryHelper( buildFriendsQuery() );

	}


	// I get called after each test method.
	public void function teardown() {

		// ...

	}


	// ---
	// TEST METHODS.
	// ---


	public void function testThatQueryHelperCanBeInitialized() {

		var queryHelper = new lib.QueryHelper();

	}


	public void function testThatQueryHelperCanBeInitializedWithQuery() {

		// NOTE: Assumes the use of "setup()" to build query.
		assert( isQuery( queryHelper.getQuery() ) );

	}


	public void function testThatQueryInMatchesQueryOut() {

		var friendsIn = buildFriendsQuery();
		var queryHelper = new lib.QueryHelper( friendsIn );

		assert(
			queryEquals( friendsIn, queryHelper.getQuery() )
		);

	}


	public void function testThatGetColumnReturnsValues() {

		var ids = queryHelper.getColumn( "id" );

		assert(
			arrayEquals( ids, [ 1, 2, 3, 4, 5 ] )
		);

		// Just make sure we get the expected failure.
		assertFalse(
			arrayEquals( ids, [ 5, 4, 3, 2, 1 ] )
		);

	}


	public void function testThatTheQueryCanBeConvertedToArray() {

		var expectedArray = [
			{ id = 1, name = "Sarah" },
			{ id = 2, name = "Joanna" },
			{ id = 3, name = "Kim" },
			{ id = 4, name = "Heather" },
			{ id = 5, name = "Tricia" }
		];

		assert(
			arrayEquals( expectedArray, queryHelper.toArray() )
		);

	}


	public void function testThatSortWorksAsc() {

		var friends = queryHelper.sort( "name" )
			.getQuery()
		;

		assert( friends.name[ 1 ] == "Heather" );
		assert( friends.name[ 2 ] == "Joanna" );
		assert( friends.name[ 3 ] == "Kim" );
		assert( friends.name[ 4 ] == "Sarah" );
		assert( friends.name[ 5 ] == "Tricia" );

	}


	public void function testThatSortWorksDesc() {

		var friends = queryHelper.sort( "name", "desc" )
			.getQuery()
		;

		assert( friends.name[ 1 ] == "Tricia" );
		assert( friends.name[ 2 ] == "Sarah" );
		assert( friends.name[ 3 ] == "Kim" );
		assert( friends.name[ 4 ] == "Joanna" );
		assert( friends.name[ 5 ] == "Heather" );

	}


	public void function testGettingMaxValueOfColumn() {

		assert( queryHelper.max( "id" ) == 5 );

	}


	// ---
	// PRIVATE METHODS.
	// ---


	private boolean function arrayEquals(
		required array a,
		required array b
		) {

		return(
			complexObjectEquals( a, b )
		);

	}


	private query function buildFriendsQuery() {

		var friends = queryNew( "" );

		queryAddColumn(
			friends,
			"id",
			"cf_sql_integer",
			[ 1, 2, 3, 4, 5 ]
		);

		queryAddColumn(
			friends,
			"name",
			"cf_sql_varchar",
			[ "Sarah", "Joanna", "Kim", "Heather", "Tricia" ]
		);

		return( friends );

	}


	private boolean function complexObjectEquals(
		required any a,
		required any b
		) {

		aData = serializeJSON( a );
		bData = serializeJSON( b );

		return( lcase( aData ) == lcase( bData ) );

	}


	private boolean function queryEquals(
		required query a,
		required query b
		) {

		return(
			complexObjectEquals( a, b )
		);

	}


}

I have to admit, there was something really nice about seeing Red turn to Green as I wrote my unit tests and then made sure they passed. I can see how this might be addictive. And, it really does give you a huge boost of confidence to know that you can change code and immediately know if you've broken something else.

Want to use code from this post? Check out the license.

Reader Comments

167 Comments

You're going to FREAK OUT if you ever move out of NY and experience the thrill of seeing a stop light go red to green from behind a steering wheel. Great post. I'm actually digging back into TDD too right now so this is great timing.

167 Comments

"And, it really does give you a huge boost of confidence to know that you can change code and immediately know if you've broken something else."

That's the biggest draw for me, but I can't help but think as I look at your code ( even though I get that it's contrived and for learning and demonstration ) that the time investment here is overwhelming. It seems like you could easily end up writing 2 to infinity times more code for each and every production "feature" you need to develop... and often more code to test than to even be tested.

And, from what I remember in examples and presentations in the past the developer really does end up writing tests as rudimentary and tedious as testThatSortWorksAsc/Desc( ).

So my question is: how does one make the argument that this stuff is tangibly worth the investment?

What portion of actual bugs and issues does it propose to "catch"?

Because, and maybe this is a limitation of how I'm perceiving it now, but writing a test that validates that a function works and passes in a query that does pass doesn't necessarily mean it'd pass if you used a different query as the argument ( say one that had a piece of text that couldn't be converted to a number or something when the first query's value could ), so unless you write a complex randomization and permutation coverage data generation engine to simulate all possibilities I don't get how these tests necessarily give the developer *that much* closure or piece of mind. I may be thinking about this wrong, I hope so.

My perspective on this btw is heavily informed by my personal development experience, which happens to be that 90-95ish percent of all bugs we hit are front-end and not CFML.

Thanks.

1 Comments

I can totally agree with the addictive side of TDD. I guess I'm from an age of instant gratification due to gamification of everything with instant rewards (especially considering the number of MMO's I play), and so having tiny goals is really motivating.

15,674 Comments

@David,

Certainly, writing the tests took a while, compared to the code that actually went into the QueryHelper.cfc. But, part of that, I think was the fact that I didn't really know what I actually wanted to do with QueryHelper. I just knew I wanted to test some stuff!

From what I've heard in some presentations, you start to get a sense of what to test and what NOT to test. And, I think you end up making fewer, but more important tests as you get more comfortable.

Sandi Metz - Ruby rockstar and long time OOP developer - just did a talk at Rails Conf 2013 about the guidelines she uses for testing:

http://www.youtube.com/watch?v=URSWYvyc42M

I had it on in the background the other day, but I really need to sit down and pay attention to it. I know that in the talk she does mention that people end up writing a lot of tests that actually make their testing life unnecessarily complicated and harder to maintain.

That said, I'm totally new to this, so everything I say is completely theoretical :D

15,674 Comments

@Shane,

Totally agree. In fact, it was exciting to create failing tests JUST so that I could then see them turn green :D I gamed myself!

167 Comments

@Ben,

Great info. I'll watch that as well as switch to Ruby ASAP, I didn't know there was such a thing as ... FEMALE DEVELOPERS ( kidding ). Thanks!

31 Comments

@David

"but writing a test that validates that a function works and passes in a query that does pass doesn't necessarily mean it'd pass if you used a different query as the argument"

A unit test makes sure that a method behaves consistently given consistent context. In this case, if you pass the method a query that is expected to give a correct result, the unit test insures that the method does work properly. If you pass in a query that is expected to give incorrect results, the unit test insures that the method gives incorrect results. Both scenarios constitute a passed unit test.

Then, as code is modified either in development or normal code maintenance, the unit tests insure that the rest of the application behaves as it did before the changes.

"It seems like you could easily end up writing 2 to infinity times more code for each and every production "feature" you need to develop... and often more code to test than to even be tested."

On the other hand, every change you subsequently make can be guaranteed to not change the behavior of other parts of the application. Uncertainty is removed and problems resulting from the change can be addressed immediately.

15,674 Comments

@David,

Yeah, the safety feels good - like I can change stuff without any fear. And, from what I've read, people get smarter about what tests they write as they get more into TDD.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel