switch (effectType)
{
case EffectType.Dog_Follow:
target.FollowName = caster.Name;
effect.caster = null;
break;
#region Strictly Bottle Effects
case EffectType.Ale:
target.SendToAllInSight(target.Name + " burps loudly.");
break;
case EffectType.Balm: // healing over time
Effect.DoBalmEffect(target, target.effectList[effectType]);
break;
case EffectType.Beer:
target.SendToAllInSight(target.Name + " burps.");
break;
Dog_Follow sounds interesting, but we’ll start with the effects which don’t need a “caster”. Ale
and Beer effects have an initial audio cue, and nothing else as far as I can tell. That should be
easy to test:
## Ale@WIPScenario: Ale effect gives the desired output - same player, beginning
Given I use the "minimal" database
And I add player and character "TestAle01"
And I give the character an ale effect of "999" for "2" turns
And the server is started
When I log on as "TestAle01"
And I enter the game
And I saw a message "You burp loudly."
@WIPScenario: Ale effect gives the desired output - same player, wearing off
Given I use the "minimal" database
And I add player and character "TestAle01"
And I give the character an ale effect of "999" for "2" turns
And the server is started
When I log on as "TestAle01"
And I enter the game
And within "2" turns I see the message "The Ale spell has worn off."
@WIPScenario: Ale effect gives the desired output - other player in same cell
Given I use the "minimal" database
And I add player and character "TestAle01"
And I give the character an ale effect of "999" for "999" turns
And I add player and character "TestAle02"
And the server is started
When I log on as "TestAle02"
And I enter the game
And I rest
Then I cause "TestAle01" to log on and enter the game
And within "4" turns I see the message "TestAle01_name burps loudly."
@WIPScenario: Ale effect gives the desired output - other player in neighboring cell
Given I use the "minimal" database
And I add player and character "TestAle01"
And I give the character an ale effect of "999" for "999" turns
And I add player and character "TestAle02"
And the server is started
When I log on as "TestAle02"
And I enter the game
And I move east
Then I cause "TestAle01" to log on and enter the game
And within "4" turns I see the message "TestAle01_name burps loudly."
Can you guess how many of these actually pass?
Failing Scenarios:
cucumber features/player_effects.feature:55 # Scenario: Ale effect gives the desired output - same player, beginning
cucumber features/player_effects.feature:75 # Scenario: Ale effect gives the desired output - other player in same cell
4 scenarios (2 failed, 2 passed)
34 steps (2 failed, 32 passed)
1m30.355s
Two things: There’s no indication to the player themselves that they burped, and the broadcast of
their burping with SendToAllInSight seems to skip characters in the same cell. But there’s a
third thing:
The ale’d character always burps twice. A quick glance at the server code indicates that the
effects may be generated twice during the character load; duplicate effects are prevented on an
ongoing basis, but the initial effect is doubled.
Of course we need to add a test to catch this bug:
Scenario: Ale effect gives the desired output - burp only once
Given I use the "minimal" database
And I add player and character "TestAle01"
And I give the character an ale effect of "999" for "999" turns
And I add player and character "TestAle02"
And the server is started
When I log on as "TestAle02"
And I enter the game
And I move east
Then I cause "TestAle01" to log on and enter the game
And within "2" turns I see the message "TestAle01_name burps loudly." only once
One of my big TODOs on the test code is to abstract away the SQL-specific code. It’s very
unlikely that the new server data stores will be SQL-based, and it’s a lot of repetition anyway.
Where do we start? A quick search shows seven separate calls to TinyTds::Client.new; let’s
refactor those so they just call connect_to_db. And to make sure we haven’t broken anything,
we’ll clear out the database server and run the tests…
Database 'minimal' does not exist. Make sure that the name is entered correctly. (TinyTds::Error)
./features/lib/db_helper_sql.rb:55:in `new'
./features/lib/db_helper_sql.rb:55:in `connect_to_db'
./features/lib/server_helper.rb:19:in `log_contains'
./features/steps/server_steps.rb:41:in `/^I allow time for the server to complete startup$/'
features/config_file.feature:10:in `And I allow time for the server to complete startup'
Oops. Turns out that renaming the databases leaves the filenames the same; so recreating them
failed, but accessing them also failed. Properly deleting them and rerunning…
After('@db_cleanup') do|scenario|
client = connect_to_db(@server_database)
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[Account]
WHERE account LIKE 'test%'
AND email LIKE '%301days.com'")
debug_msg "Rows deleted from [Account]: #{result.do}"
result = client.execute("SELECT playerID, account, name
FROM [#{@server_database}].[dbo].[Player]
WHERE account LIKE 'test%'
AND name LIKE 'Dude.%'")
players_to_delete =[]
result.each(:symbolize_keys=>true) do|rowset|
players_to_delete << rowset[:playerID]end
debug_msg "Players to delete: #{players_to_delete}"
player_tables =["PlayerBelt","PlayerEffects","PlayerFlags","PlayerHeld","PlayerLocker",
"PlayerQuests","PlayerRings","PlayerSack","PlayerSettings","PlayerSkills","PlayerSpells",
"PlayerWearing","Player"]
players_to_delete.each do|player_to_delete|
debug_msg "Deleting player #{player_to_delete}"
player_tables.each do|table|
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[#{table}]
WHERE playerID = '#{player_to_delete}'")
debug_msg "Rows deleted from [#{table}]: #{result.do}"endend
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[Log]")
debug_msg "Rows deleted from [Log]: #{result.do}"end
There’s a SELECT in here to find the playerIDs of the test accounts, so we can delete the
appropriate entries in the related tables. Let’s move that into db/player.rb:
defget_all_test_player_ids()
id_list =[]
client = connect_to_db(@server_database)
query ="SELECT [playerID] FROM [#{@server_database}].[dbo].[Player] \
WHERE account LIKE 'test%' \
AND name LIKE 'Dude.%'"
result = client.execute(query)
result.each(:symbolize_keys=>true) do|rowset|
id_list << rowset[:playerID]end
id_list
end
But before we simply replace the code in hooks.rb, we’ll do what I like to call
“The Safety Dance”, where we query both ways and assert that the
results are the same:
After('@db_cleanup') do|scenario|
client = connect_to_db(@server_database)
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[Account]
WHERE account LIKE 'test%'
AND email LIKE '%301days.com'")
debug_msg "Rows deleted from [Account]: #{result.do}"
result = client.execute("SELECT playerID, account, name
FROM [#{@server_database}].[dbo].[Player]
WHERE account LIKE 'test%'
AND name LIKE 'Dude.%'")
players_to_delete =[]
result.each(:symbolize_keys=>true) do|rowset|
players_to_delete << rowset[:playerID]end
debug_msg "Players to delete: #{players_to_delete}"
debug_msg "All test player IDs: #{get_all_test_player_ids()}"fail"Player ID issue"if players_to_delete.sort != get_all_test_player_ids().sort
Then we run some tests that use this hook, watching out for failures due to the two methods not
getting the same set of user IDs. Luckily, we don’t.
Why do this, though? Normally we use tests as a safety net when we refactor application code, but
what about when we refactor the test code? Instead of writing a test suite for the test suite (and
then a test suite for that test suite, etc), we take care to temporarily insert these little
bespoke test clauses to make sure we’re still doing what we want.
Once we’re satisfied, we complete this part of the refactor:
After('@db_cleanup') do|scenario|
client = connect_to_db(@server_database)
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[Account]
WHERE account LIKE 'test%'
AND email LIKE '%301days.com'")
debug_msg "Rows deleted from [Account]: #{result.do}"
player_tables =["PlayerBelt","PlayerEffects","PlayerFlags","PlayerHeld","PlayerLocker",
"PlayerQuests","PlayerRings","PlayerSack","PlayerSettings","PlayerSkills","PlayerSpells",
"PlayerWearing","Player"]
get_all_test_player_ids().each do|player_to_delete|
debug_msg "Deleting player #{player_to_delete}"
player_tables.each do|table|
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[#{table}]
WHERE playerID = '#{player_to_delete}'")
debug_msg "Rows deleted from [#{table}]: #{result.do}"endend
result = client.execute("DELETE
FROM [#{@server_database}].[dbo].[Log]")
debug_msg "Rows deleted from [Log]: #{result.do}"end
It didn’t take that much to move any SELECTs out of non-db-specific code. We’ll think about
tackling the DELETEs and UPDATEs too. Tomorrow.