Indholdsfortegnelse
Jeg (Simon) udarbejder oftest afprøvningskode inden jeg går igang med at implementere koden. Jeg tilføjer et tomt afprøvningstilfælde, forsøgt navngivet efter hvad jeg har til hensigt med at teste. Jeg kigger på testcasen og langsomeligt får jeg manifesteret mine tanker i form af testkode. Når jeg føler at det som jeg har til hensigt med at testes, bliver tilstrækkeligt grundigt afprøvet i testcasen, så kan jeg gå igang med at implementere. Jeg skriver kode indtil at testcasen passer OK, hvor jeg så straks kan kaste mig over det næste punkt i todo-listen.
Sjældent går det så let som ovenfor og man vil altid komme ud for ubehagelige overraskelser. Vi vil derfor vise et detaljeret eksempel på hvordan paradigmet fungere i praksis. Allerførst skal man gøre sig nogle ideer om hvad man agter at lave.
Reglerne er følgende:
En befolket celle, med 0..1 naboer, dør af ensomhed.
En befolket celle, med 2..3 naboer, overlever.
En befolket celle, med >= 4 naboer, dør af overbefolkning.
En tom celle, med 3 naboer, bliver befolket.
Lad os tage udgangspunkt i disse regler.
require 'test/unit'
class TestGameOfLife < Test::Unit::TestCase
def determine_destiny(alive, count)
# not yet implemented
end
def test_destiny_populated
data = [
[0, false],
[1, false],
[2, true],
[3, true],
[4, false],
[5, false],
[6, false]
]
input, expected = data.transpose
actual = input.map do |count|
determine_destiny(true, count)
end
assert_equal(expected, actual)
end
def test_destiny_empty
data = [
[0, false],
[1, false],
[2, false],
[3, true],
[4, false],
[5, false],
[6, false]
]
input, expected = data.transpose
actual = input.map do |count|
determine_destiny(false, count)
end
assert_equal(expected, actual)
end
end
require 'test/unit/ui/console/testrunner'
Test::Unit::UI::Console::TestRunner.run(TestGameOfLife)
Loaded suite TestGameOfLife Started FF Finished in 0.029156 seconds. 1) Failure: test_destiny_empty(TestGameOfLife) [a.rb:36]: <[false, false, false, true, false, false, false]> expected but was <[nil, nil, nil, nil, nil, nil, nil]>. 2) Failure: test_destiny_populated(TestGameOfLife) [a.rb:20]: <[false, false, true, true, false, false, false]> expected but was <[nil, nil, nil, nil, nil, nil, nil]>. 2 tests, 2 assertions, 2 failures, 0 errors
Lad os prøve at implementere determine_destiny.
def determine_destiny(alive, count)
unless alive
return (count == 3)
end
(count == 2) or (count == 3)
end
Loaded suite TestGameOfLife Started .. Finished in 0.00158 seconds. 2 tests, 2 assertions, 0 failures, 0 errors
Så langt så godt, vi har nu behov for en rutine som kan tælle antal tilstødende celler som er befolkede. Lad os lave noget testkode først.
def count_neighbours(cells, x, y)
# not yet implemented
end
def test_count_neighbours0
cells = [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
]
n = count_neighbours(cells, 1, 1)
assert_equal(0, n)
end
def test_count_neighbours1
cells = [
[1, 0, 0],
[0, 1, 1],
[0, 1, 0]
]
n = count_neighbours(cells, 1, 1)
assert_equal(3, n)
end
def test_count_neighbours2
cells = [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0]
]
n = count_neighbours(cells, 1, 1)
assert_equal(6, n)
end
Loaded suite TestGameOfLife Started FFF.. Finished in 0.027788 seconds. 1) Failure: test_count_neighbours0(TestGameOfLife) [a.rb:50]: <0> expected but was <nil>. 2) Failure: test_count_neighbours1(TestGameOfLife) [a.rb:59]: <3> expected but was <nil>. 3) Failure: test_count_neighbours2(TestGameOfLife) [a.rb:68]: <6> expected but was <nil>. 5 tests, 5 assertions, 3 failures, 0 errors
Lad os implementere metoden count_neighbours.
def count_neighbours(cells, x, y) n = 0 n += cells[y-1][x-1] n += cells[y-1][x] n += cells[y-1][x+1] n += cells[y][x-1] n += cells[y][x+1] n += cells[y+1][x-1] n += cells[y+1][x] n += cells[y+1][x+1] n end
Loaded suite TestGameOfLife Started ..... Finished in 0.002225 seconds. 5 tests, 5 assertions, 0 failures, 0 errors
Lad os lave noget testkode for gennemlevning af en livscyklus.
def lifecycle(cells)
# not yet implemented
end
def test_lifecycle1
cells = [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0]
]
expected_cells = [
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
]
actual = lifecycle(cells)
assert_equal(expected_cells, actual)
end
Loaded suite TestGameOfLife Started .....F Finished in 0.024648 seconds. 1) Failure: test_lifecycle1(TestGameOfLife) [a.rb:95]: <[[1, 0, 1], [0, 0, 0], [1, 0, 1]]> expected but was <nil>. 6 tests, 6 assertions, 1 failures, 0 errors
Implementering...
def lifecycle(cells)
next_cells = cells.map do |row|
row.map do |cell|
9
end
end
next_cells
end
Loaded suite TestGameOfLife Started .....F Finished in 0.027937 seconds. 1) Failure: test_lifecycle1(TestGameOfLife) [a.rb:100]: <[[1, 0, 1], [0, 0, 0], [1, 0, 1]]> expected but was <[[9, 9, 9], [9, 9, 9], [9, 9, 9]]>. 6 tests, 6 assertions, 1 failures, 0 errors
Vi har fat i noget af det rigtige.
def lifecycle(cells)
y = 0
next_cells = cells.map do |row|
x = 0
next_row = row.map do |cell|
n = count_neighbours(cells, x, y)
x += 1
n
end
y += 1
next_row
end
next_cells
end
Loaded suite TestGameOfLife
Started
.....E
Finished in 0.005263 seconds.
1) Error:
test_lifecycle1(TestGameOfLife):
TypeError: nil can't be coerced into Fixnum
a.rb:45:in `+'
a.rb:45:in `count_neighbours'
a.rb:85:in `lifecycle'
a.rb:84:in `map'
a.rb:84:in `lifecycle'
a.rb:82:in `map'
a.rb:82:in `lifecycle'
a.rb:105:in `test_lifecycle1'
6 tests, 5 assertions, 0 failures, 1 errors
Boom, vi har et problem med count_neighbours. Hvad er det lige som er problemet her? Lad os lave nogle testcases som exerciser count_neighbours, sådan at samme problem fremprovokeres.
def test_count_neighbours3
cells = [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0]
]
n = count_neighbours(cells, 0, 0)
assert_equal(3, n)
end
def test_count_neighbours4
cells = [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0]
]
n = count_neighbours(cells, 2, 2)
assert_equal(3, n)
end
Loaded suite TestGameOfLife
Started
...FE..E
Finished in 0.02368 seconds.
1) Failure:
test_count_neighbours3(TestGameOfLife) [a.rb:87]:
<3> expected but was
<7>.
2) Error:
test_count_neighbours4(TestGameOfLife):
TypeError: nil can't be coerced into Fixnum
a.rb:45:in `+'
a.rb:45:in `count_neighbours'
a.rb:95:in `test_count_neighbours4'
3) Error:
test_lifecycle1(TestGameOfLife):
TypeError: nil can't be coerced into Fixnum
a.rb:45:in `+'
a.rb:45:in `count_neighbours'
a.rb:103:in `lifecycle'
a.rb:102:in `map'
a.rb:102:in `lifecycle'
a.rb:100:in `map'
a.rb:100:in `lifecycle'
a.rb:123:in `test_lifecycle1'
8 tests, 6 assertions, 1 failures, 2 errors
Første problem skyldes at opslag i Array med negativt index, hvorved vi så tilgå Array'et baglæns. Andet problem skyldes at vi prøvet at tilgå elementer udenfor Arrayet, hvorved der så returneres nil. Altså har vi "glemt" tage højde for grænsesituationerne. Det må vi se om vi kan løse. Lad os foretage følgende omskrivning.
def get(cells, x, y) return 0 if x < 0 or y < 0 return 0 if y >= cells.size row = cells[y] return 0 if x >= row.size row[x] end def count_neighbours(cells, x, y) n = 0 n += get(cells, y-1, x-1) n += get(cells, y-1, x) n += get(cells, y-1, x+1) n += get(cells, y, x-1) n += get(cells, y, x+1) n += get(cells, y+1, x-1) n += get(cells, y+1, x) n += get(cells, y+1, x+1) n end
Loaded suite TestGameOfLife Started .......F Finished in 0.027889 seconds. 1) Failure: test_lifecycle1(TestGameOfLife) [a.rb:131]: <[[1, 0, 1], [0, 0, 0], [1, 0, 1]]> expected but was <[[3, 4, 3], [4, 6, 4], [3, 4, 3]]>. 8 tests, 8 assertions, 1 failures, 0 errors
Meget bedre, nu kan vi forsætte med implementeringen af lifecycle. Af test uddata kan fornemme at vi har glemt kalde determine_destiny. Lad os kalde den.
def lifecycle(cells)
y = 0
next_cells = cells.map do |row|
x = 0
next_row = row.map do |cell|
n = count_neighbours(cells, x, y)
x += 1
determine_destiny((cell != 0), n) ? 1 : 0
end
y += 1
next_row
end
next_cells
end
Loaded suite TestGameOfLife Started ........ Finished in 0.004913 seconds. 8 tests, 8 assertions, 0 failures, 0 errors
Vi har nu næsten et funktionsdygtigt game of life program.