Skip to content
On this page

My Take on TDD

August 21, 2021

Roughly ten years or so ago when I was working on Java 1.5 projects I was taught that testing was a “nice to have” and not a major concern. Although this always felt wrong, I hated writing tests and usually just went a long with it. We would write code, test it manually on a test server and then release it into the wild, and that generally worked for us.

I always had a bad feeling about it, but I was a junior at the time working directly with a CTO and some senior devs, so I wasn't about to complain.

If anything went wrong it was kinda their problem..

Unfortunately for me this left me with some bad habits. I found my self consistantly writing logic, and pushing my changes only to have my manager message me with "where are the tests?". TDD has helped me get out of this habit.

The idea behind it is by writing down the tests first you can use them essentually as instructions for you logic.

General concept

The general idea behind test driven development is to write tests before doing the actual development. The thinking is that this will get the programmer thinking about what is actually required in a broad sense, and thus using the tests to drive the logic development.

The official process is:

  • Add a test
  • Run all tests to check if it fails
  • Write some logic
  • Run the tests
  • Refactor
  • Repeat

And thats just great!, In my experience most software practises should't be taken so seriously and can be slightly adjusted, which is what I have done.

Add A Test

add a test is not as easy as it sounds, you can spend a long time on this point. In my opinion the best place to start is with the test description, a good description can make writing the test a lot easier.

Lets say I have been tasked with writing a pallendrome function, the function will take some input and return a boolean value, true if the input is a pallendrome and false if it isn't. Lets write some test signitures for this function:

typescript
it('should return true when input is a pallendrome');
it('should return false when input is not a pallendrome');
it('should return true when input is a sentance and is a pallendrome');
it('should return true when input is a pallendrome regardless of casing');

By writing these signitures we have already defined the shell of our logic, we know the input may be a single word or a sentance, we know the casing should't affect the outcome. These scenarios could have been an after thought if we wern't implimenting TDD.

Run all tests to check if it fails

This step always seems pointless to me. If I was doing TDD the way The Man wants me too I would have added some logic to my tests. Personally I don't see the point, it will fail as I dont have the actaul logic yet. In some cases I might have the logic already written, i.e. if I was patching something etc, in that case I would have added logic, but when adding something new I usually skip this step.

Write Some Logic

Woooop this is the best bit! And thanks to our test signitures we have a good idea of what it is we need to do, we need a function which returns a boolean, true if the input is a pallendrome, false if it's not, ingore casing and handle sentances.

typescript
export class PalindromeChecker {
    function isPallendrome(input:string): boolean {
        return str.split('').reverse().join('') === input;
    }
}

Run The Tests (Write The Tests)

This step changes to Write The Tests as we dont actaully have any logic in our tests at the moment, but now we have a class PalindromeChecker and a function isPallendrome to test against.

typescript
it('should return true when input is a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('dad')).toBeTruthy();
});

it('should return false when input is not a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('date')).toBeFalsy();
});

it('should return true when input is a pallendrome regardless of casing', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('Dad')).toBeTruthy();
});

it('should return true when input is a sentance and is a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('do geese see god')).toBeTruthy();
});

The first two tests pass and the last two fail because we don't yet have the logic for ignoring casing and sentances

Refactor

Now we go back to our logic and refactor. It's important we dont try to fix all the tests at once, much more simple if we take the first faling test and refactor the logic to make it pass and then go back around for the next test.

typescript
export class PalindromeChecker {
    function isPallendrome(input:string): boolean {
        const result str.split('').reverse().join('');
        return result.toLowerCase() === input.toLowerCase();
    }
}

So now we're reversing the string and storing it in a varible named result. We are lowering the casing on the result varible and the input string and then returning the output of comparison.

Repeat

So now we run the tests again:

typescript
it('should return true when input is a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('dad')).toBeTruthy();
});

it('should return false when input is not a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('date')).toBeFalsy();
});

it('should return true when input is a pallendrome regardless of casing', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('Dad')).toBeTruthy();
});

it('should return true when input is a sentance and is a pallendrome', () => {
    const palindromeChecker = new PalindromeChecker();
    expect(palindromeChecker.isAPalindrome('do geese see god')).toBeTruthy();
});

Woooop a previously failing test is now passing! So now we repeat by adding more logic to isAPalindrome function to get the last test to pass.

ta da!

Conclusion

Previously I found writing tests to be a bit of a pain, but with TDD it's actaully enjoyable. Writing logic to get the tests to pass is fun, where as writing tests against logic that already exists is not.