Blog

24
December 2013

Gavin Pickin

Unit Testing 02 - TDD - Lets Write Tests for our Objects, then Build them to Pass the Tests

Unit Testing

So we're back for another post in my Getting Started with Unit Testing. In the first post, we got our Testing Environment Setup, so we could browse to our project, then to the first blog, and the first step in that blog, where we could see we had 2 Test Bundles (or Suites) with 1 dummy test in each, copied from the documentation. 

In this post, we're going to remove those dummy or sample tests, and actually write some tests for our new objects. Our objects are actually empty so far, so we'll write some tests, then test them, they'll fail, and then we'll add some code to our objects, to get them to pass. This all part of the Test Driven Development process. I will not explain the pros and cons here in detail, but in short, if you write your tests to cover your specifications, and edge cases, your code will always be better, you can maintain it easier because all of your tests are there, for any member of the team (whether they know the specs and or edge cases) and because your code has to be testable, it should be shorter, single function methods, and just more DRY in general.

Reminder of the setup.

Ok, so, for all of you out there, the Github repo is setup.
https://github.com/gpickin/Blog-UnitTesting-Series Star, Fork, Clone, Follow Me, all the GitHub actions you want.

Local will be
http://Blog-UnitTesting-Series.local.com
or http://buts.local.com

Dev Server will be (when I get it setup in the new few days)
http://buts.gpickin.com 

I will not note all the project setup, like naming the index.cfm h1 tags, and adding the links, as thats all unimportant in the scheme of things.

Ok, so I've created a duplicate of the Blog 01 - Step1 from our last post, and we'll start from there. 

Step 1 - Remove the Dummy / Sample Tests from our Test Suites

First thing we're going to do is remove the testIncludesWithCase function / test from the /blog02/Step1/test/unit/CFMLServer.cfc file.
Next we are going to remove the testIncludes function / test from the /blog02/Step1/test/unit/Website.cfc file.

We are also going to update our tests to extend the TestBox BaseSpec. This allows the tests to do a lot more nuts and bolts stuff, especially with MxUnit styled tests -  extends="testbox.system.testing.BaseSpec" - Note, this fixes the isExpectedException error you might see if you do not have this.

Now we are just left with our basic shell, and they'll look like this (apart from the display name of course).

component displayName="Website.cfc Unit Tests" extends="testbox.system.testing.BaseSpec" {

    // executes before all tests
    function beforeTests() {}]

    // executes after all tests
    function afterTests() {}

}

Now, we're going to browse and run our Empty Tests, and see the results.
You should see something like this.

We have 2 bundles, 2 suites, but no specs / tests? so 0 pass 0 fail 0 error. Now we are moving onto Step 2

Step 2 - Add Tests to Test if Our Objects Initialize Correctly

We're going to work with Website.cfc first, so we open /blog02/Step2/test/unit/Website.cfc and add a function called testObjectInit(). It is going to assert that the variable we pass it, Website, is of type component. The code will look like this

component displayName="Website.cfc Unit Tests" extends="testbox.system.testing.BaseSpec" {

    // executes before all tests
    function beforeTests() {}

    // executes after all tests
    function afterTests() {}

    function testObjectInit() {
        $assert.typeOf( "component", Website );
    }

}

You might wonder where the Website variable is coming from, and you're right, we haven't created it, but we're going to run the test to see what an error looks like. Your tests might have errors as you write them, and its important to know what the look like, and how to determine how to fix them. So lets browse to our project, click on blog-02, Step2 and see what the results are.

You will see that the summary has 2 bundles, 2 suites, 1 spec, 0 pass, 0 fail, 1 error. If you scroll down through the bundles, you'll see CFMLServer.cfc is ok, we haven't edited that, but Website.cfc has the error. The variable WEBSITE doesn't exist. There is also " + " button, if you click that, it expands, and shows you the stack trace, and allows you to determine your error (normally, not in this case), and fix it. Below is an example of the Stack Trace, its running on Railo, so the colors are not the usual Adobe ColdFusion blues.

Ok, now we're going to edit the Website.cfc located in /blog-02/Step2/test/unit/ folder, and we're going to add some code to the beforeTests() function. This code runs before the tests, as the name explains, and allows you to setup for your tests, so your tests can share some code. In this case, we're going to Init the component. Note, these functions are global, this means beforeTests() is run one time before all these tests in this bundle, and afterTests() is run one time after all the tests. If you need a variable or fresh set of data for each test, you can set those in two functions, called setup() and tearDown() which are performed before and after each test respectively. Since setup() and tearDown() are called for each test, these 2 functions receive an argument called "currentMethod" so if you need to change the setup and tearDown depending on the method, you can easily do so.

In this case, we're going to add the component creation to the beforeTests(), so we can use it throughout our tests. To do so, we add the following code to the Website.cfc Unit Test in the beforeTests() function

Website = new blog02.Step2.model.Website().init();

Remember, each iteration we do, needs to update the Step inside this initialization as well. I'll be doing that each time, so if you're following along, and your test doesn't do what it should, make sure the setup() is creating the right object from the right Step.

Lets browse, and see what the results of our tests are now.

Now, we get an error stating there is no function called init in that component. We haven't created it yet. Since this Error was in the beforeTests() function, the tests do not even run. So, lets add the init function, just an empty one for now, so our tests do not error. Lets add the following code

function init() {}

If we run our tests again, we get an error, because our init method does not pass anything back to be stored in our Website variable. Let's follow best practices and return this, which is the component. That way we can continue to work with the reference to this component.

function init() { return this; }

Now when we run our tests, we test that our Website variable is a component, and indeed it is. The .init() call returns this, which is a reference to the component itself.

Step 3 - Create a Test for Method count() in Website.cfc

We're going to create our first real Method in our Website.cfc (other than the best practice mandatory init() of course)? count(). This function gives us a count of the number of websites we have. Our specification for this method is pretty simple right now, when we call this function, we expect an integer back, greater than equal to 0. We're testing the functionality of the method, not how it does its job, so we don't care if it has a database, a flat file, or like right now, this method will return hard coded numbers.

So, lets add the new test in blog02/Step3/test/unit/Website.cfc called testCount() and we'll add the assertions we need to match our specifications.

function testCount() {
    var count = Website.count();
    $assert.typeOf( "numeric", count );
}

Ok, so what we have done here is add a function, called testCount(), in that method, we call the count() method on our Global Website object created in setup(), and store it in the variable count. We store it in a variable, because we're going to use it several times. So our first Assertion, is we assert that count is typeof "numeric". Lets save it, and run our first. Browse to blog02 - Step3

You can see in the Website bundle, we have 1 suite, and 2 tests. 1 Pass testObjectInit(), 0 fails, 1 error testCount(). The error is because we wrote the spec, without building the function / method in our object yet. So, thats the next step.

Step 4 - Create the Method count() in Website.cfc

Now we're going to create the actual count() method in Website.cfc that our test is testing. So open up the file /blog02/Step4/model/Website.cfc

If we add this count() function, 

function count() {
    return "there are 15 websites";
?}

The test errors, because the return is not numeric.
Lets change it to 

function count() {
    return 1.5;
}

And now, Blog 02 - Step4 passes 2 tests.

Step 5 - Add Second Spec / Assertion to testCount() Test

Now we're going to add a Second Spec, to test the count() method more fully. We're going to check and see if its not a floating point number? and that the number is greater than 0, since we cannot have 1.5 websites, or -5 websites. 

Just to clarify, I am going back and forward on purpose to show you piece by piece the steps involved. Once you get into the groove, you might right 8-10 specs in 4-5 tests, and then run the tests, see them fail, and then go and right the methods to satisfy them. You do not and should not do it spec by spec.

function testCount() {
    var count = Website.count();
    $assert.typeOf( "numeric", count );
    $assert.assert( int(count) == count, "An integer was expected, but " & count & " was received");
    $assert.assert( count >= 0 );
}

Now you can see we have 3 assertions, first, ensuring we are returned a numeric value, then we test to ensure its not a float, and then we assert that its greater than or equal to 0. Lets run the tests, and see what happens. You might wonder what is that message after our float assertion? It is a way you can pass a useful message back to your test, so you know what the test failed. You can leave the generic message, or insert your own. Here, I put the actual value of the count into the message using concatenation. 

Now, if we go back to our /blog02/Step5/model/Website.cfc you can see we were returning 1.5, which of course fails the not float test. Lets update it to -5 and see if our next test fails.
Change from

function count() {
    return 1.5;
}

Change to

function count() {
    return -5;
}

Run the tests

Did you see that message, Expected False to be True. That is not a very friendly message, so lets add our own message to that test. We'll update, it so it looks like this

$assert.assert( count >= 0 , "Expected " & count & " to be greater than 0");

You can see that is almost self documenting, it makes the next Developer who looks at your test understand what was expected, and what was actually received, and makes it easier to fix the issue at hand.

Now, we're going to return a valid count, the number 10. It is numeric, not a float, and greater than 0, it should pass.

function count() {
    return 10;
}

All the tests pass. I've left all the return statements in my code, on github, so you can just uncomment the different return values, so you can see how the tests pass and fail differently.

Now we have build our count() method, and our testCount() method to make sure it works correctly, but its not a very good method, if the count() is hard coded. The beauty of Unit Testing is, when you change the way your functions work, your Unit Tests can make sure your modifications still provide valid responses with the Tests you have already written.

We have covered quite a lot today, including our own flavor to our unit testing with our custom messages. This is quite a long post, so I didn't get it all done and released as quick as I had hoped. Our next post will walk through taking the count() method, and changing the way it gets that value it returns. We'll look at a couple of different ways to do it, so check back then and follow along.

 

Thanks,

Gavin

 

 

by Sean Corfield
12/24/2013 11:23:00 AM

Just FYI, you don't need to call .init() on a new'd object: new calls init() for you if it exists. If you do new blog02.Step2.model.Website().init(); you'll call init() twice.

However, if you have new blog02.Step2.model.Website(); and you don't have an init function, it will silently not call it (so your initial test would pass).

by Gavin
12/26/2013 08:43:02 AM

Thanks Sean, one for reading, two for keeping me in check, and three, always need advice.

I have been touching up on my OO, and the few sites I have gone through had the init called on the end of the creation.

http://www.iknowkungfoo.com/ http://objectorientedcoldfusion.org/ They show a few different ways to create objects, but maybe they're just showing their age, they all had init, but I'm not sure if they were using the new syntax.

Now I'm curious, I might have to test out a few things.

Thanks Sean :)

Blog Search