Part two solving a problem using a TDD-based approach, Yayy!
This is the second article (this is the first) where I document my process solving a problem using TDD. These posts are not really helpful if you want to copy the code I am writing, because I have had to obscure the problem I am working on as it is an old interview question. I'll have to write another series where I can show how to come up with abstract concepts to help your design so that you can see it before it has been abstracted. There are a lot of lightbulb moments in these posts that could be helpful though.
So far, we have two objects. The TetrisGame object has a limit of 0 blocks, and has no room for more blocks. These are hard-coded values to pass the test. We will improve them later. The Block object has a default size of 0, but the size can be set to any number.
Block size
The next test is to prevent the Block from accepting a negative value, as follows:
it 'not allow a negative number size' do
block = Block.new -2
expect(block.size).to eql('Block sizes must be a positive number.')
end
Oops, my test failed because I forgot to add a 'do' to the end of the 'it' description. Added and corrected above.
def initialize( size = 0 )
raise 'Block size must be a positive number.' if size < 0
@size = size
end
class Block
attr_reader :size
def initialize( size = 0 )
raise 'Block size must be a positive number.' if size < 0
@size = size
end
end
The code above raises an error if the size is set to a negative number. The test failed even though the error message appeared as expected, which means that the test is not testing for error messages raised correctly. I'll look it up.
I struggled to figure out how to fix the test, the documentation wasn't clear to me, so I asked a colleague to help. Turns out I just needed to add curly brackets to indicate that the instantiation of a new object is a function to be called, which raises an error.
it 'not allow a negative number size' do
expect{Block.new -2}.to raise_error('Block size must be a positive number.')
end
Now the test passes. Okay, so I have a Tetris Game with a block limit set to 0. It doesn't accept new blocks, and I have a block whose size can be set to a positive number. The next most important thing is to go back to the 'is there room for blocks' set of tests, because we now have a block with a size that we can compare against the limit to see if there is room. Here are the tests we have for the TetrisGame so far:
describe 'TetrisGame should' do
it 'have a limited number of blocks' do
tetris = TetrisGame.new
expect(tetris.blockLimit).to eql(0)
end
it 'tell us if there is room to add more blocks' do
tetris = TetrisGame.new
expect(tetris.isThereRoomForBlocks).to eql(false)
end
end
I'm going to start by doing a bit of refactoring of the tests above. The first thing I'm going to change is the name of the first one, which instead of saying 'TetrisGame should have a limited number of blocks', I want it to read 'TetrisGame should have a default block limit of 0'. The other way of phrasing suggest that the game holds a limited number of blocks, instead of accepting a limited number of blocks. The test still passes here.
Tetris game block limit
The next test will push us in the direction of allowing a custom block limit, as follows:
it 'have a custom block limit' do
tetris = TetrisGame.new 1
expect(tetris.blockLimit).to eql(1)
end
Code which passes the above test:
def initialize( blockLimit = 0 )
@blockLimit = blockLimit
end
Next, I want the tetris game to throw an error if a negative number is set as the blockLimit.
it 'not allow blockLimit to be set to a negative number' do
expect{TetrisGame.new -1}.to raise_error('Block limit must be a positive number.')
end
Code to pass this test:
def initialize( blockLimit = 0 )
raise 'Block limit must be a positive number.' if blockLimit < 0
@blockLimit = blockLimit
end
Is there room in the game for a new block?
The next most important thing to work on is checking to see if there is enough room to add a new block to our tetris game.
At the moment, our tetris game block limit is set to 0 by default, which means that there is no room for more blocks. Our current test (see below) passes. However, it uses a hard-coded value to pass. Now we have a block with a size, we can compare it to the limit instead of using our hard-coded value, which was written before we had a block to compare with.
it 'tell us if there is room to add more blocks' do
tetris = TetrisGame.new
expect(tetris.isThereRoomForBlocks).to eql(false)
end
Before I implement the new code, I'm going to refactor the test name and method name from 'isThereRoomForBlocks' to 'isThereRoomForThis' wit an added parameter 'block', so it will read 'isThereRoomForThis(block)
I also changed all of the method names to snake_case to match Ruby's naming convention. The new implementation code is as follows:
def is_there_room_for_this(block)
if block.size > @block_limit
false
end
end
Next, I want to check that a block with a size of 1 can be added if the block limit is bigger than the block size.
it 'tell us if there is room to add another block' do
tetris = TetrisGame.new 1
expect(tetris.is_there_room_for_this(Block.new 1)).to eql(true)
end
The code that passes the test:
def is_there_room_for_this(block)
if block.size > @block_limit
false
else
true
end
end
The next test is to say there is no room for a block if it would go over the block limit (into minus numbers):
it 'tell us if there is room for another block' do
tetris = TetrisGame.new 1
expect(tetris.is_there_room_for_this(Block.new 3)).to eql(false)
end
The code to pass the test:
def is_there_room_for_this(block)
if block.size > @block_limit || @block_limit - block.size < 0
false
else
true
end
end
There are a couple of things I would like to refactor. First, there are three tests that test for the same thing, using different values. So I'm going to use an each loop to call the test on each set of values. Secondly, I'm going to use a guard clause to remove the else clause in the 'if/else' condition statement in the solution.
When I tried to parameratize the tests, I found that the tests were not actually doing the same thing, so I changed their names to reflect what they were actually doing. The new tests names are:
- TetrisGame will not have room for a block if the block limit is 0
- TetrisGame will have room for a block if it is smaller than the block limit size
- TetrisGame will not have room for a block if it would bring the block limit below 0
Instead of using a guard clause, I just returned a negated condition, as follows:
def is_there_room_for_this(block)
!(block.size > @block_limit || @block_limit - block.size < 0)
end
Oh, wait. I'm an actual idiot. I changed the code to this:
def is_there_room_for_this(block)
block.size <= @block_limit
end
If the block size is smaller than the block limit, of course it isn't going to take the limit into minus numbers once subtracted...
Now we have a TetrisGame with a limited number of blocks it can hold, and a block of a specific size. We can check to see if there is room for the block to be added to the game.