Writing test code is a worthwhile practice and building a parser is a good example to prove this claim. We have seen previously that the Parser
class consists of many methods and each method is a part of a chain in top-down analysis. It won’t be a good idea to start writing the parser from scratch, add all methods we think necessary and then run the code for the first time. Instead of tempting fate, we should write tests for every line of code we create.
The parser code was showed in the previous part at once. The fact is that I wrote it step by step, adding a new functionality and test code every time. I run the test often and fix any errors as soon as they appeared, before proceeding to the next functionality. Perhaps illustrating this process would be very instructive for those not familiar with test driven development, but I meant to focus on the parser itself. Anyway, here is the test code I wrote:
require 'parser'
describe Parser do
before(:each) do
@parser = Parser.new
end
it 'should compute 5 when given 2 + 3' do
@parser.parse('2 + 3').should == 5
end
it 'should compute 6 when given 2 * 3' do
@parser.parse('2 * 3').should == 6
end
it 'should compute 89 when given 89' do
@parser.parse('89').should == 89
end
it 'should raise an error when input is empty' do
lambda {@parser.parse('')}.should raise_error()
end
it 'should omit white spaces' do
@parser.parse(' 12 - 8 ').should == 4
@parser.parse('142 -9 ').should == 133
@parser.parse('72+ 15').should == 87
@parser.parse(' 12* 4').should == 48
@parser.parse(' 50/10').should == 5
end
it 'should treat dot separated floating point numbers as a valid input' do
@parser.parse('2.5').should == 2.5
@parser.parse('4*2.5 + 8.5+1.5 / 3.0').should == 19
@parser.parse('5.0005 + 0.0095').should be_close(5.01, 0.01)
end
it 'should handle tight expressions' do
@parser.parse('67+2').should == 69
@parser.parse(' 2-7').should == -5
@parser.parse('5*7 ').should == 35
@parser.parse('8/4').should == 2
end
it 'should calculate long additive expressions from left to right' do
@parser.parse('2 -4 +6 -1 -1- 0 +8').should == 10
@parser.parse('1 -1 + 2 - 2 + 4 - 4 + 6').should == 6
end
it 'should calculate long multiplicative expressions from left to right' do
@parser.parse('2 -4 +6 -1 -1- 0 +8').should == 10
@parser.parse('1 -1 + 2 - 2 + 4 - 4 + 6').should == 6
end
it 'should calculate long, mixed additive and multiplicative expressions from left to right' do
@parser.parse(' 2*3 - 4*5 + 6/3 ').should == -12
@parser.parse('2*3*4/8 - 5/2*4 + 6 + 0/3 ').should == -1
end
it 'should return float pointing numbers when division result is not an integer' do
@parser.parse('10/4').should == 2.5
@parser.parse('5/3').should be_close(1.66, 0.01)
@parser.parse('3 + 8/5 -1 -2*5').should be_close(-6.4, 0.01)
end
it 'should raise an error on wrong token' do
lambda {@parser.parse(' 6 + c')}.should raise_error()
lambda {@parser.parse(' 7 & 2')}.should raise_error()
lambda {@parser.parse(' %')}.should raise_error()
end
it 'should raise an error on syntax error' do
lambda {@parser.parse(' 5 + + 6')}.should raise_error()
lambda {@parser.parse(' -5 + 2')}.should raise_error()
end
it 'should return Infinity when attempt to divide by zero occurs' do
@parser.parse('5/0').should be_infinite
@parser.parse(' 2 - 1 + 14/0 + 7').should be_infinite
end
it 'should compute 2 when given (2)' do
@parser.parse('(2)').should == 2
end
it 'should compute complex expressions enclosed in parenthesis' do
@parser.parse('(5 + 2*3 - 1 + 7 * 8)').should == 66
@parser.parse('(67 + 2 * 3 - 67 + 2/1 - 7)').should == 1
end
it 'should compute expressions with many subexpressions enclosed in parenthesis' do
@parser.parse('(2) + (17*2-30) * (5)+2 - (8/2)*4').should == 8
@parser.parse('(5*7/5) + (23) - 5 * (98-4)/(6*7-42)').should be_infinite
end
it 'should handle nested parenthesis' do
@parser.parse('(((((5)))))').should == 5
@parser.parse('(( ((2)) + 4))*((5))').should == 30
end
it 'should raise an error on unbalanced parenthesis' do
lambda {@parser.parse('2 + (5 * 2')}.should raise_error()
lambda {@parser.parse('(((((4))))')}.should raise_error()
lambda {@parser.parse('((2)) * ((3')}.should raise_error()
lambda {@parser.parse('((9)) * ((1)')}.should raise_error()
end
end
Test code is written in the format accepted by the RSpec framework (any other test framework could be used as well). Each test covers the other part of functionality: those parts that come directly from the parser specification (like “it should compute 6 when given 2 * 3”) and those part that might go wrong, like “it should calculate long additive expressions from left to right”. Test code has to be stored in a file, e.g. parser_spec.rb
. To run the spec, type this at the command line:
spec parser_spec.rb --format specdoc
The “–format specdoc” can be shortened to:
spec parser_spec.rb -f s
Anyway, spec output should be similar to this:
Parser
- should compute 5 when given 2 + 3
- should compute 6 when given 2 * 3
- should compute 89 when given 89
- should raise an error when input is empty
- should omit white spaces
- should treat dot separated floating point numbers as a valid input
- should handle tight expressions
- should calculate long additive expressions from left to right
- should calculate long multiplicative expressions from left to right
- should calculate long, mixed additive and multiplicative expressions from left to right
- should return float pointing numbers when division result is not an integer
- should raise an error on wrong token
- should raise an error on syntax error
- should return Infinity when attempt to divide by zero occurs
- should compute 2 when given (2)
- should compute complex expressions enclosed in parenthesis
- should compute expressions with many subexpressions enclosed in parenthesis
- should handle nested parenthesis
- should raise an error on unbalanced parenthesis
Finished in 0.018552 seconds
19 examples, 0 failures
We end up with a properly working and quite functional parser. It can be further developed or create a base for a completely different program, like some simple XML or YAML parser.
I hope you enjoyed this short series of articles. Thanks for reading!
Go back to part 3: Implementation.