DEV Community

Yigal Ziskand
Yigal Ziskand

Posted on • Edited on

Palindrom implementation with TDD'sh approach

Disclaimer

Before we start i would like to make a disclaimer - we are not going to dig into the holly war of speed vs quality in software development terms, neither we will be comparing tests approaches.

Motivation

We want to find a handy approach for testing our code - some magical way that won't require spending extra time and afford for testing.

Basic Idea

Let's break down the way we (or should i say i...) usually approach new problem.

  1. acknowledge the problem by going over it's details
  2. figure out the way to solve the problem - logical solution
  3. provide code implementation for the logical solution
  4. validate solution correctness

Hmm... let's try to switch step 3 and 4 and see what we got

  1. acknowledge the problem by going over it's details
  2. figure out the way to solve the problem - logical solution
  3. validate solution correctness
  4. provide code implementation for the logical solution

Sweet! So this is how it works! Simply do the tests before you write your code...
Hmmm, hold on a second - what do we test exactly, there's no code to test yet... weird situation...

Well... The answer is a bit philosophical - after we accomplished step 1 & 2 you should find yourself in the position where we have a complete logical solution to the problem and by saying that - you know the exact logical flow and its logical boundaries!

That is exactly what we need!

First we will write tests for the logical solution! Then we will execute the tests (and surprisingly they will fail... i guess it makes scene, since there is no actual code implementations at this point)
And finally, in order to make the tests pass we will add the code implementation.

This way we can be sure that our code implementation does exactly what we had targeted in step 2

Example (Numerical Palindrome)

Let's peek up a problem of defining numerical Palindrome Object which is self sufficient in a way that:

  • it can be created with any input type
  • it can be questioned on its value
  • it can return whether it is a valid numerical palindrome

So let's break it down into 1,2,3,4 steps:

  1. The details of the description are the following:
    • input type: any
    • the object should manage it's internal state
    • provide public methods
      • getter(): returns initial input values
      • isValid(): return boolean
  2. Pseudo code for logical solution:
    // provided in requirements
    if user_input not number return false
    // negative number cant be palindrome
    if user_input is less then 0 return false
    // any positive number in range of 1 to 10 is valid palindrome
    if user_input is in range of 1..10 return user_input

    // if number is bigger then 10, 
    // then we shall gradually divide our initial user_input into
    // left-comparison half & right-comparison half
    // once we divided into two halfs
    // we shall compare the halfs and return the comparison result
    while left-comparison-half > right-comparison-half
        // collect the most right number from user_input
        // to the right-comparison half
        right-comparison-half: collect user_input's most right number

        // remove the most right number from the left-comparison half
        left-comparison-half: = remove user_input's most right number


    // compare the collected halfs and return the result
    return left-comparison-half === right-comparison-half
Enter fullscreen mode Exit fullscreen mode
  1. Let's write our expectation from the logical solution

I am using Jest library npm i -D jest
You might find useful to adjust your test command to watch over the files so the tests will be re-executed on every file change
"scripts": { "clear_jest": "jest --clearCache", "test": "jest --watchAll --verbose" },

describe("Numeric Palindrome", () => {
    it.todo("should be initialized with any input type")
    it.todo("should be able to manage it's state")
    it.todo("validation method should be defined")
    it.todo("return false if data is not numeric")
    it.todo("return false if negative number")
    it.todo("return false if data is 10 dividable")
    it.todo("return true if data is smaller then 10")
    it.todo("return true if legal palindrome")
    it.todo("return false if not legal palindrome")
})
Enter fullscreen mode Exit fullscreen mode

Great start!

It's important to mention that no matter how scary-spaghetti our code will be, we know one thing for sure - it will be well defined Palindrome!

- Let's make our first test fail, by modifying

it.todo("should be initialized with any input type")
Enter fullscreen mode Exit fullscreen mode

- into:

    it("should be initialised with any input type",
        () => {
            const palindromInstances = [
                new Palindrome("abc"),
                new Palindrome(),
                new Palindrome(1),
                new Palindrome({})
            ]

            palindromInstances.forEach(instance => expect(instance).toBeDefined())
        }
    );
Enter fullscreen mode Exit fullscreen mode

and if we look at our test result we will find the exact reasons
First Failing Test
Yes, of course we should create a proper Palindrome class and define it's constructor, so lets do it

class Palindrome {
    constructor() { }
}

module.exports = Palindrome
Enter fullscreen mode Exit fullscreen mode

and of course don't forget to import it into our test

const Palindrome = require('./numeric-palindrome')

describe("Numeric Palindrome", () => {
Enter fullscreen mode Exit fullscreen mode

Well done, we got our first test fulfilled. Let's continue with the next one...
- modify:

it.todo("should be able to manage it's state")
Enter fullscreen mode Exit fullscreen mode

- into:

    it("should be able to manage it's state", () => {
        const palindromeOne = new Palindrome('1');
        const palindromeTwo = new Palindrome();
        const palindromeThree = new Palindrome(1);

        expect(palindromeOne).toHaveProperty("data", "1");
        expect(palindromeTwo).toHaveProperty("data", "");
        expect(palindromeThree).toHaveProperty("data", 1);
    })
Enter fullscreen mode Exit fullscreen mode

check why the test failed and adjust the Palindrome implementation with a getter method and a default value

class Palindrome {
    constructor(userInput = '') {
        this._data = userInput
    }

    get data() {
        return this._data
    }
}
Enter fullscreen mode Exit fullscreen mode

Yaay - the test passes, Let's move to the next one...
- modify:

it.todo("validation method should be defined")
Enter fullscreen mode Exit fullscreen mode

- into:

    it("validation method should be defined", () => {
        const palindrome = new Palindrome()

        expect(palindrome.isValid()).toBeDefined()
    })
Enter fullscreen mode Exit fullscreen mode

and of course it fails... So let's fix it

class Palindrome {
    constructor(userInput = '') {
        this._data = userInput
    }

    get data() {
        return this._data
    }

    isValid() {
        return false
    }
}
Enter fullscreen mode Exit fullscreen mode

Good job, we've made it again... Let's move on
- modify:

it.todo("return false if data is not numeric")
Enter fullscreen mode Exit fullscreen mode

- into:

   it("return false if data is not numeric", () => {
        const notNumeric = [new Palindrome("a"), new Palindrome(), new Palindrome({})]

        notNumeric.forEach(x => expect(x.isValid()).toBeFalsy())
    })
Enter fullscreen mode Exit fullscreen mode

check the failed test and fix the implementation....

class Palindrome {
    constructor(userInput = '') {
        this._data = userInput
    }

    get data() {
        return this._data
    }

    isValid() {
        if (!Number.isInteger(this._data)) {
            return false
        }

        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

and once again, let's go into our next test requirement
- modify:

it.todo("return false if negative number")
Enter fullscreen mode Exit fullscreen mode

- into:

 it("return false if negative number", () => {
     const negativeNumber = new Palindrome(-1)

     expect(negativeNumber.isValid()).toBeFalsy()
 })
Enter fullscreen mode Exit fullscreen mode

check the failed test and fix the implementation....

isValid() {
        if (!Number.isInteger(this._data)) {
            return false
        }

        if (this._data < 0) {
            return false
        }

        return true
    }
Enter fullscreen mode Exit fullscreen mode

Well i think at this point you got the idea of how it works and how it looks...

In summery:
- Create the test that should checking some condition in your logical solution
- Execute it and check the failing reasons
- Adjust the code implementation so the test pass
- And don't forget to refactor

Code Snippet

I did not refactor the code at any point so every additional line is followed by the corresponded test requirement - i hope this way you can follow the test-fail-implement process easier

// requiriments
const Palindrome = require('./numeric-palindrome')

describe("Numeric Palindrome", () => {
    it("should be initialised with any input type",
        () => {
            const palindromInstances = [
                new Palindrome("abc"),
                new Palindrome(),
                new Palindrome(1),
                new Palindrome({})
            ]

            palindromInstances.forEach(instance => expect(instance).toBeDefined())
        }
    );
    it("should be able to manage it's state", () => {
        const palindromeOne = new Palindrome('1');
        const palindromeTwo = new Palindrome();
        const palindromeThree = new Palindrome(1);

        expect(palindromeOne).toHaveProperty("data", "1");
        expect(palindromeTwo).toHaveProperty("data", "");
        expect(palindromeThree).toHaveProperty("data", 1);
    })
    it("validation method should be defined", () => {
        const palindrome = new Palindrome()

        expect(palindrome.isValid()).toBeDefined()
    })
    it("return false if data is not numeric", () => {
        const notNumeric = [new Palindrome("a"), new Palindrome(), new Palindrome({})]

        notNumeric.forEach(x => expect(x.isValid()).toBeFalsy())
    })
    it("return false if negative number", () => {
        const negativeNumber = new Palindrome(-1);

        expect(negativeNumber.isValid()).toBeFalsy();
    })
    it("return false if data is 10 devidable", () => {
        const tenDivision = [new Palindrome(10), new Palindrome(20), new Palindrome(150)];

        tenDivision.forEach(sample => expect(sample.isValid()).toBeFalsy())
    })
    it("return true if data is smaller then 10", () => {
        const underTen = [new Palindrome(1), new Palindrome(2), new Palindrome(9)];

        underTen.forEach(sample => expect(sample.isValid()).toBeTruthy())
    })
    it("return false if not legal palindrome", () => {
        const invalidPalindromes = [new Palindrome(1112), new Palindrome(112), new Palindrome(12)]

        invalidPalindromes.forEach(sample => expect(sample.isValid()).toBeFalsy())
    })
    it("return true if legal palindrome", () => {
        const validPalindromes = [new Palindrome(111), new Palindrome(11), new Palindrome(1)]

        validPalindromes.forEach(sample => expect(sample.isValid()).toBeTruthy())
    })
})
Enter fullscreen mode Exit fullscreen mode
// implementation
class Palindrome {
    constructor(userInput = '') {
        this._data = userInput
    }

    get data() {
        return this._data
    }

    isValid() {
        if (!Number.isInteger(this._data)) {
            return false
        }

        if (this._data < 0) {
            return false
        }

        if (this._data % 10 === 0) {
            return false
        }

        if (this._data < 10) {
            return true
        }

        let leftPart = this.data
        let rightPart = 0

        while (leftPart > rightPart) {
            // reserve extra space for additional number
            rightPart *= 10
            // add the most right number
            rightPart += leftPart % 10
            // remove the most right number from the left-part
            leftPart = Math.trunc(leftPart / 10)
        }

        // compare left and right parts in case left and right part have equal number of digits
        // compare left and right parts in case right part has collected the digit in the middle
        return leftPart === rightPart || leftPart === Math.trunc(rightPart / 10)
    }
}

module.exports = Palindrome
Enter fullscreen mode Exit fullscreen mode

Repo

https://github.com/ziskand/code-katas

Resources

Top comments (0)