h2. What to put in your tests \-\- or, patterns for your assertions
Components are a mix of state and behavior. Thus, when we test, we typically want to know:
* Did the state of the object change as expected?
* Did the behavior occur that I expected?
Sometimes we want to know one or the other; sometimes we want to know both.
Here are some "assertion patterns" to help you get started. \**The terms here are taken from the outstanding book "Test-Driven" by Lasse Koskela.
h3. Resulting-State Assertion
The resulting-state assertion tests data. It says "I'm doing something to my object that will change that object's data, or 'state'. I'm going to test that the resulting state of my object is as I expect". A simple example is the common "bank account" or "transaction" example: You have two accounts, you transfer $20 from one account to another, and you test that the first account is 20 bucks shorter and the second account has that 20 bucks. Here's a different example, using a typical "User" object:
{code:title=Resulting-State Assertion}<cffunction name="testAddPrivilegeGroup">
<cfset user.addPrivilegeGroup("admin")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.belongsToGroup("admin"))>
</cffunction>
<cffunction name="testAddPrivilege">
<cfset user.addPrivilege("DeleteOtherUsers")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.hasPrivilegeTo("DeleteOtherUsers"))>
</cffunction>
{code}
h3. Guard Assertion
The guard assertion is simply a slight variant on the resulting state assertion; typically, the difference is that toward the top of the test, before you get into the "guts" of your assertion(s), you check the object for some condition that you want to ensure exists before proceeding with the meat of your tests. Think of it as "If this condition isn't true, I want to fail right now because the rest of the tests don't matter". Usually the "guard" is just a simple assertion for equality, often to check that a "default" condition exists. In our example here, we're simply checking that our user object always starts without the privileges we're adding. IF the user object already had those privileges, then that means something tinkered with our object (perhaps indicating a problem in our setup function) and we got a problem here, Houston, and we want to fail right now.
{code:title=Guard Assertion}<cffunction name="testAddPrivilegeGroup">
<!--- our 'guard': ensure we are starting fresh --->
<cfset assertFalse(user.belongsToGroup("admin"))>
<cfset user.addPrivilegeGroup("admin")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.belongsToGroup("admin"))>
</cffunction>
<cffunction name="testAddPrivilege">
<!--- our 'guard': ensure we are starting fresh --->
<cfset assertFalse(user.hasPrivilegeTo("DeleteOtherUsers"))>
<cfset user.addPrivilege("DeleteOtherUsers")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.hasPrivilegeTo("DeleteOtherUsers"))>
</cffunction>
{code}
h3. Different instances, same data
The different-instances, same-data pattern is common in DAO testing. Essentially, we're asserting that two objects are different instances but contain the same data. In MXUnit, you can test for "different instance" by using the assertNotSame() assertion.
{code:title=Different instances, same data}<cffunction name="testSaveWithRead">
<cfset dao.save(user)>
<cfset user2 = dao.read(user.getUserID())>
<!--- assert they're not the same instance --->
<cfset assertNotSame(user,user2)>
<!--- one final check... assert the UUIDs aren't the same --->
<cfset assertTrue( user.getUUID() neq user2.getUUID(), "user UUID and user2 UUID should not be the same but are")>
<cfset assertEquals(user.getUserID(),user2.getUserID())>
<cfset assertEquals(user.getCreateTS(),user2.getCreateTS())>
<!--- is there an easier way? could we use any of the functions in the baseobject for comparisons so we don't have to test all these fields? --->
<cfset assertEquals(user.XXXXX,user2.XXXXX)>
</cffunction>
{code}
h3. "Delta" Assertion
Sometimes you can't assert an absolute equality (like "My list is now 5 elements long"). Sometimes, you have to assert equality relative to some previous state. In the example below, imagine you're hooking into some scheduling mechanism (this would be for an integration test, for example). We don't know exactly what getTotalScheduled() will return at any given test run. Maybe it's 1. Maybe it's 30. Who knows. What we want to test is that when we schedule one additional thing, our scheduler's "totalScheduled" count increases by 1. Again, the result could be 2 or it could be 31. We don't know. This type of assertion, where we compare the state right before and right after performing some task, is called "delta", or difference, assertion.
{code:title=Delta Assertion}<cffunction name="testScheduleSomething">
<cfset obj = createObject("component","SchedulableObject")>
<cfset currentlyScheduled = scheduler.getTotalScheduled()>
<cfset scheduler.scheduleSomething(obj)>
<cfset assertEquals(currentlyScheduled+1, scheduler.getTotalScheduled())>
</cffunction>
{code}
And here's what it might look like for our User object:
{code:title=Delta Assertion on User object}<cffunction name="testPrivilegeCount">
<cfset currentPrivilegeCount = user.getPrivilegeCount()>
<cfset user.addPrivilege("ChangeOwnPassword")>
<cfset assertEquals(currentPrivilegeCount+1, user.getPrivilegeCount())>
</cffunction>
{code}
h3. Interaction Assertion
With interaction assertions, we're testing to make sure an object and a collaborator worked together as we expected. A great example of an interaction is a "bean" style object, like perhaps a "User", and the DAO for that object, like a UserDAO:
{code:title=Interaction Assertion}<cffunction name="testSave"
<cfset origUUID = user.getUUID()>
<cfset dao.save(user)>
<cfset newUserID = user.getUserID()>
<!--- Assert that our DAO updated our user object with its ID after saving --->
<cfset assertTrue(newUserID GT 0)>
<cfset user.setEmail("[email protected]")>
<cfset dao.save(user)>
<!--- We don't care about testing the update here... just that the insert didn't change the user object's key --->
<cfset assertEquals(newUserID,user.getUserID())>
<cfset assertEquals(origUUID,user.getUUID())>
</cffunction>
{code}
h3. Testing for Expected Exceptions
Frequently, you want to test the "error paths" in your code. You want to ensure that functions throw Exceptions under certain conditions. To test this, use the mxunit:expectedException attribute on your test's cffunction tag:
{code:title=ExpectedException}
<cffunction name="testMakePublicNonExistentMethod" mxunit:expectedException="Application">
<!--- do something here that you expect to throw an Exception --->
<cfset this.maker.makePublic(this.objectWithPrivateMethod,"aPrivateMethodThatDoesNotExist")>
</cffunction>
{code}
You can pass a list of expected exceptions in the expectedException attribute:
{code:title=ExpectedException list}
<cffunction name="b_shouldFailBecauseExpectedExceptionListNotThrown" mxunit:expectedException="Database,MyCustomException">
<cfset x = doSomethingThatMightThrowADatabaseOrMyCustomException()>
</cffunction>
{code}
In CFScript:
{code:title=ExpectedException in CFScript}
/**
* @mxunit:expectedException Database
*/
function when_query_bombs_database_error_is_thrown(){
dao.doSomethingStupidThatThrowsADatabaseError();
}
As of MXUnit 2.0, a separate method has been added {{expectException("some.exception");}}. This can be used in place of the annotation, but _must_ be the first statement in your test.
{note}
It was implemented for developers who prefer cfscript, but use CFML engines that do not yet support annotations in script.
{note}
Example:
{code:title=expectException() method}
function shouldThrowMyException(){
expectException("my.exception");
doSomethingThatMightThrowAMyException();
}
{code}
Components are a mix of state and behavior. Thus, when we test, we typically want to know:
* Did the state of the object change as expected?
* Did the behavior occur that I expected?
Sometimes we want to know one or the other; sometimes we want to know both.
Here are some "assertion patterns" to help you get started. \**The terms here are taken from the outstanding book "Test-Driven" by Lasse Koskela.
h3. Resulting-State Assertion
The resulting-state assertion tests data. It says "I'm doing something to my object that will change that object's data, or 'state'. I'm going to test that the resulting state of my object is as I expect". A simple example is the common "bank account" or "transaction" example: You have two accounts, you transfer $20 from one account to another, and you test that the first account is 20 bucks shorter and the second account has that 20 bucks. Here's a different example, using a typical "User" object:
{code:title=Resulting-State Assertion}<cffunction name="testAddPrivilegeGroup">
<cfset user.addPrivilegeGroup("admin")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.belongsToGroup("admin"))>
</cffunction>
<cffunction name="testAddPrivilege">
<cfset user.addPrivilege("DeleteOtherUsers")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.hasPrivilegeTo("DeleteOtherUsers"))>
</cffunction>
{code}
h3. Guard Assertion
The guard assertion is simply a slight variant on the resulting state assertion; typically, the difference is that toward the top of the test, before you get into the "guts" of your assertion(s), you check the object for some condition that you want to ensure exists before proceeding with the meat of your tests. Think of it as "If this condition isn't true, I want to fail right now because the rest of the tests don't matter". Usually the "guard" is just a simple assertion for equality, often to check that a "default" condition exists. In our example here, we're simply checking that our user object always starts without the privileges we're adding. IF the user object already had those privileges, then that means something tinkered with our object (perhaps indicating a problem in our setup function) and we got a problem here, Houston, and we want to fail right now.
{code:title=Guard Assertion}<cffunction name="testAddPrivilegeGroup">
<!--- our 'guard': ensure we are starting fresh --->
<cfset assertFalse(user.belongsToGroup("admin"))>
<cfset user.addPrivilegeGroup("admin")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.belongsToGroup("admin"))>
</cffunction>
<cffunction name="testAddPrivilege">
<!--- our 'guard': ensure we are starting fresh --->
<cfset assertFalse(user.hasPrivilegeTo("DeleteOtherUsers"))>
<cfset user.addPrivilege("DeleteOtherUsers")>
<!--- verify the internal state of the user --->
<cfset assertTrue(user.hasPrivilegeTo("DeleteOtherUsers"))>
</cffunction>
{code}
h3. Different instances, same data
The different-instances, same-data pattern is common in DAO testing. Essentially, we're asserting that two objects are different instances but contain the same data. In MXUnit, you can test for "different instance" by using the assertNotSame() assertion.
{code:title=Different instances, same data}<cffunction name="testSaveWithRead">
<cfset dao.save(user)>
<cfset user2 = dao.read(user.getUserID())>
<!--- assert they're not the same instance --->
<cfset assertNotSame(user,user2)>
<!--- one final check... assert the UUIDs aren't the same --->
<cfset assertTrue( user.getUUID() neq user2.getUUID(), "user UUID and user2 UUID should not be the same but are")>
<cfset assertEquals(user.getUserID(),user2.getUserID())>
<cfset assertEquals(user.getCreateTS(),user2.getCreateTS())>
<!--- is there an easier way? could we use any of the functions in the baseobject for comparisons so we don't have to test all these fields? --->
<cfset assertEquals(user.XXXXX,user2.XXXXX)>
</cffunction>
{code}
h3. "Delta" Assertion
Sometimes you can't assert an absolute equality (like "My list is now 5 elements long"). Sometimes, you have to assert equality relative to some previous state. In the example below, imagine you're hooking into some scheduling mechanism (this would be for an integration test, for example). We don't know exactly what getTotalScheduled() will return at any given test run. Maybe it's 1. Maybe it's 30. Who knows. What we want to test is that when we schedule one additional thing, our scheduler's "totalScheduled" count increases by 1. Again, the result could be 2 or it could be 31. We don't know. This type of assertion, where we compare the state right before and right after performing some task, is called "delta", or difference, assertion.
{code:title=Delta Assertion}<cffunction name="testScheduleSomething">
<cfset obj = createObject("component","SchedulableObject")>
<cfset currentlyScheduled = scheduler.getTotalScheduled()>
<cfset scheduler.scheduleSomething(obj)>
<cfset assertEquals(currentlyScheduled+1, scheduler.getTotalScheduled())>
</cffunction>
{code}
And here's what it might look like for our User object:
{code:title=Delta Assertion on User object}<cffunction name="testPrivilegeCount">
<cfset currentPrivilegeCount = user.getPrivilegeCount()>
<cfset user.addPrivilege("ChangeOwnPassword")>
<cfset assertEquals(currentPrivilegeCount+1, user.getPrivilegeCount())>
</cffunction>
{code}
h3. Interaction Assertion
With interaction assertions, we're testing to make sure an object and a collaborator worked together as we expected. A great example of an interaction is a "bean" style object, like perhaps a "User", and the DAO for that object, like a UserDAO:
{code:title=Interaction Assertion}<cffunction name="testSave"
<cfset origUUID = user.getUUID()>
<cfset dao.save(user)>
<cfset newUserID = user.getUserID()>
<!--- Assert that our DAO updated our user object with its ID after saving --->
<cfset assertTrue(newUserID GT 0)>
<cfset user.setEmail("[email protected]")>
<cfset dao.save(user)>
<!--- We don't care about testing the update here... just that the insert didn't change the user object's key --->
<cfset assertEquals(newUserID,user.getUserID())>
<cfset assertEquals(origUUID,user.getUUID())>
</cffunction>
{code}
h3. Testing for Expected Exceptions
Frequently, you want to test the "error paths" in your code. You want to ensure that functions throw Exceptions under certain conditions. To test this, use the mxunit:expectedException attribute on your test's cffunction tag:
{code:title=ExpectedException}
<cffunction name="testMakePublicNonExistentMethod" mxunit:expectedException="Application">
<!--- do something here that you expect to throw an Exception --->
<cfset this.maker.makePublic(this.objectWithPrivateMethod,"aPrivateMethodThatDoesNotExist")>
</cffunction>
{code}
You can pass a list of expected exceptions in the expectedException attribute:
{code:title=ExpectedException list}
<cffunction name="b_shouldFailBecauseExpectedExceptionListNotThrown" mxunit:expectedException="Database,MyCustomException">
<cfset x = doSomethingThatMightThrowADatabaseOrMyCustomException()>
</cffunction>
{code}
In CFScript:
{code:title=ExpectedException in CFScript}
/**
* @mxunit:expectedException Database
*/
function when_query_bombs_database_error_is_thrown(){
dao.doSomethingStupidThatThrowsADatabaseError();
}
As of MXUnit 2.0, a separate method has been added {{expectException("some.exception");}}. This can be used in place of the annotation, but _must_ be the first statement in your test.
{note}
It was implemented for developers who prefer cfscript, but use CFML engines that do not yet support annotations in script.
{note}
Example:
{code:title=expectException() method}
function shouldThrowMyException(){
expectException("my.exception");
doSomethingThatMightThrowAMyException();
}
{code}