Blog

02
January 2014

Gavin Pickin

Unit Testing 03 - More TDD - Building out our Component Objects

Unit Testing

The first couple of Unit Testing posts have got us setup with TestBox and we have written our first real tests for our components. Now it is time to build up our components even more, and in the TDD way, we'll create a few specs, create the tests, then build the methods in our components to pass the tests.

If you haven't followed along, you can go back to Unit Testing 01, and Unit Testing 02, and get the files from my github repo, and then follow along. Step 0 is our base, where we left off from last time, Step 1 is about to begin for blog post 03.

Step 1 - Add testValidDomainName() Test

This app is the start of our Website Manager tool, to help us maintain information about websites, which server they are running on, what cfml engine, and of course, they have domain names attached to them. We need to ensure we validate the domain names so they follow domain name design, but since you could be working with a hosts file for your domain names, not all the rules apply, but these are the ones we're going to look for.

  • Does the domain name only contain letters, numbers, dashes "-" and periods "."
  • Does the domain name segments (sections delimited by periods) Start with Letters
  • Does the domain name segments end with Letters
  • Does each segment have at least one character

We could obviously check for length of the segments, but without more testing into what the hosts file will allow, at this time, we'll assume any length greater than or equal to 1 is sufficient.
So, lets create our test for these assertions.

The Website Object will be the one validating Domains for a Website, so we'll open blog03/step01/test/Website.cfc to add the new test.

function testValidDomainName() {

$assert.assert( Website.isValidDomainName("myTestDomain.com") == true, "True was expected, but False was returned - This is a valid Domain name");
$assert.assert( Website.isValidDomainName("myTestDomain?com") == false, "False was expected, but True was returned - Question Marks are Invalid Characters");
$assert.assert( Website.isValidDomainName("myTestDomain/com") == false, "False was expected, but True was returned - Forward Slash are Invalid Characters");
$assert.assert( Website.isValidDomainName("myTestDomain com") == false, "False was expected, but True was returned - Spaces are Invalid Characters");
$assert.assert( Website.isValidDomainName("1myTestDomain.com") == false, "False was expected, but True was returned - Segments must start with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain.2com") == false, "False was expected, but True was returned - Segments must start with a letter");
$assert.assert( Website.isValidDomainName("-myTestDomain.com") == false, "False was expected, but True was returned - Segments must start with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain.-com") == false, "False was expected, but True was returned - Segments must start with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain1.com") == false, "False was expected, but True was returned - Segments must end with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain.com2") == false, "False was expected, but True was returned - Segments must end with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain-.com") == false, "False was expected, but True was returned - Segments must end with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain.com-") == false, "False was expected, but True was returned - Segments must end with a letter");
$assert.assert( Website.isValidDomainName("myTestDomain..com") == false, "False was expected, but True was returned - Segment must have at least one Character");
$assert.assert( Website.isValidDomainName("myTestDomain.com.") == false, "False was expected, but True was returned - Segment must have at least one Character");
$assert.assert( Website.isValidDomainName(".myTestDomain.com") == false, "False was expected, but True was returned - Segment must have at least one Character");

}

 

Now, you can see, there are a lot of assertions here. They are all testing the same method, the isValidDomainName method, all with different criteria. They are edge cases that you might throw at your code manually when testing a form, what if I do this, what if I do that. If you were doing this in a browser, it would take a lot of time, to try each of these out, but it only took a few mins to write these up, and now anytime we change anything, we can run these tests and make sure our validation logic is solid.

So we go and run the test now, and… since this is TDD, it fails :)

Step 2 - Add isValidDomainName() Method

So now we're going to go and add the method to the Website.cfc component.
So we open the file blog03/step2/model/Website.cfc

First we add the method, and set it to return true always… which is the default response, and we'll add validation shortly to return false if any of the reasons why the passed in Domain name is invalid are found.

function isValidDomainName(domainName) {
    return true;
}

 

Now, we run the tests, and we see, 1 of our assertions passed, and the rest fail.
Our first assertion is that if we pass a valid Domain name, it will be valid, and return true, this is correct, so this assertion passes.

Our next assertion, we pass a domain name with a question mark in it. 
This is supposed to pass back false, this is not a valid domain name. Since our method currently only passes back a TRUE, this fails, as you can see by the screenshot below.

Now, we want to add some logic, that checks the passed in Domain Name for invalid characters.
So we update our method to check for invalid characters

function isValidDomainName(domainName) {
    if (refind('[^A-Za-z\-\..]', domainName)) {
        return false;
    }
    return true;
}

 

Now, this should catch any domain name with anything other than A-Z, a-z, dash - and period .
So we run our tests again, and where does it fail now?

As you can see, it fails on this assertion - False was expected, but True was returned - Segments must start with a letter
That is our 4th assertion. So our if statement just passed 3 assertions, ?, / and a space " ". That is encouraging. 

Step 3 - Lets check our Domain Name Segment for Starting and Ending Characters

Now, we need to check each segment, and see if they all begin with a letter… so we're going convert the domain name from a list with a period delimiter, into an array, and loop over it. And we'll check the left most character, and see if its one of our A-Z or a-z characters.

function isValidDomainName(domainName) {
    var i = "";
    var segmentArray = "";

    if (refind('[^A-Za-z\-\..]', domainName)) {
        return false;
    }

    segmentArray = listToArray(domainName, ".", true);
    for (i = 1; i <= arrayLen(segmentArray); i++){
        if (refind('[^A-Za-z]', left(segmentArray[i],1))) {
            return false;
        }
    }

    return true;
}

 

Now, we save this, and run our tests again, and the failing assertion is - False was expected, but True was returned - Segments must end with a letter.

That is our 9th assertion, which means that new validation check catches 4 false domain names, and returns false correctly.
Now, we need to do the same type of check for the last letter of a segment, so we'll just add it to the loop, to be efficient.

So after the first if statement in the for loop, add the following if statement.

if (refind('[^A-Za-z]', right(segmentArray[i],1))) {
    return false;
}

 

Now the failing assertion message is - False was expected, but True was returned - Segment must have at least one Character… which is our last assertion, so that change correctly identified another 4 invalid domain names. The last assertion we need to check for, is empty segments. 

Now, there is a slight trick to this… usually when you convert lists to arrays, it ignores empty items, but the TRUE on the end of the listToArray function is includeEmptyFields. Without that, our code would never see an empty segment.

Ok, so while we're in the same for loop, we'll add another if statement, this time checking the length of the segment, if its 0 we will return false. If it is 1 or more, for our specs, it is valid.

if (len(segmentArray[i],1) == 0) {
    return false;
}

Now, we run our tests, and what do we see?

A big fat error… because I missed something, when I copied and pasted, and edited the above if statement. Yup, I make mistakes, and you might too, so I modify the if statement, so the len function is passed a string as the only argument.

if (len(segmentArray[i]) == 0) {
    return false;
}

 

And now, as you can see below, the tests pass.

We have successfully added the test to make sure our isValidDomainName function exists, and works to our specifications. Its a pretty simple example, but it shows how you can make sure you check off all the specs as you go through, and every new spec just adds to the list, making your code better and better all the time.

Check back soon for the next post in the series, soon we're going to look at databases, services and mocking, to make our Unit Testing more thorough than the basics we've touched on so far. 

Thanks for reading,

Gavin

 

Blog Search