14
Palindrom implementation with TDD'sh approach
Before we start i would like to make a disclaimer - we are not going to to dig into the holly war of speed vs quality in software development terms, neither we will comparing tests approaches.
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.
Let's break down the way we (or should i say i...) usually approach new problem.
- acknowledge the problem by going over it's details
- figure out the way to solve the problem - logical solution
- provide code implementation for the logical solution
- validate solution correctness
Hmm... let's try to switch step 3 and 4 and see what we got
- acknowledge the problem by going over it's details
- figure out the way to solve the problem - logical solution
- validate solution correctness
- 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
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:
- 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
- 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
- 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")
})
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")
- 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())
}
);
and if we look at our test result we will find the exact reasons
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
and of course don't forget to import it into our test
const Palindrome = require('./numeric-palindrome')
describe("Numeric Palindrome", () => {
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")
- 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);
})
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
}
}
Yaay - the test passes, Let's move to the next one...
- modify:
it.todo("validation method should be defined")
- into:
it("validation method should be defined", () => {
const palindrome = new Palindrome()
expect(palindrome.isValid()).toBeDefined()
})
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
}
}
Good job, we've made it again... Let's move on
- modify:
it.todo("return false if data is not numeric")
- 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())
})
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
}
}
and once again, let's go into our next test requirement
- modify:
it.todo("return false if negative number")
- into:
it("return false if negative number", () => {
const negativeNumber = new Palindrome(-1)
expect(negativeNumber.isValid()).toBeFalsy()
})
check the failed test and fix the implementation....
isValid() {
if (!Number.isInteger(this._data)) {
return false
}
if (this._data < 0) {
return false
}
return true
}
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
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())
})
})
// 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
14