Day 38 - Testy Housekeeping

Coding

Cleanup - database

So we’ve got a small set of working tests, but after a while there’s a problem..

hmm
1
2
3
SELECT * FROM [DSpineBackup].[dbo].[Account] WHERE account LIKE 'test%' AND email LIKE '%301days.com'
  
 (1018 row(s) affected)

We’re cluttering up our database something fierce, and at some point we’re bound to hit a limit on the server. We need to clean up after ourselves, which means the test code has to hit the database. So we add an After hook to the Cucumber code.

hooks.rblink
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
After('@db_cleanup') do |scenario|
  connect_hash = {username: ENV['DB_USERNAME'], password: ENV['DB_PASSWORD'], dataserver: ENV['DB_DATASERVER'], database: ENV['DB_NAME']}
  puts "After hook connecting to #{ENV['DB_DATASERVER']} database #{ENV['DB_NAME']}..."
  client = TinyTds::Client.new(connect_hash) 
  puts "  Connected." if client.active?
  result = client.execute("DELETE
    FROM [#{ENV['DB_NAME']}].[dbo].[Account]
    WHERE account LIKE 'test%'
    AND email LIKE '%301days.com'")
  puts "Rows deleted from [Account]: #{result.do}"
  result = client.execute("SELECT playerID, account, name
    FROM [#{ENV['DB_NAME']}].[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
  puts "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|
  	puts "Deleting player #{player_to_delete}"
  	player_tables.each do |table|
	  result = client.execute("DELETE
		FROM [#{ENV['DB_NAME']}].[dbo].[#{table}]
		WHERE playerID = '#{player_to_delete}'")
	  puts "Rows deleted from [#{table}]: #{result.do}"
	end
  end
end

As long as we have all of the appropriate environment variables set, and the @db_cleanup tag on the test, all of the test accounts/characters will get cleaned up after the test runs.

      After hook connecting to AQUALUNG\SQLEXPRESS database DSpineBackup...
        Connected.
      Rows deleted from [Account]: 1
      Players to delete: [3005]
      Deleting player 3005
      Rows deleted from [PlayerBelt]: 8
      Rows deleted from [PlayerEffects]: 20
      Rows deleted from [PlayerFlags]: 1
      Rows deleted from [PlayerHeld]: 2
      Rows deleted from [PlayerLocker]: 20
      Rows deleted from [PlayerQuests]: 0
      Rows deleted from [PlayerRings]: 8
      Rows deleted from [PlayerSack]: 21
      Rows deleted from [PlayerSettings]: 1
      Rows deleted from [PlayerSkills]: 1
      Rows deleted from [PlayerSpells]: 35
      Rows deleted from [PlayerWearing]: 20
      Rows deleted from [Player]: 1
d:\>sqlcmd -S AQUALUNG\SQLEXPRESS -U sa -d DSpineBackup -Q "SELECT
account FROM [DSpineBackup].[dbo].[Account] WHERE account LIKE 'test%' AND email
 LIKE '%301days.com'"
 
account
--------------------

(0 rows affected)

Of course our test code just got tightly coupled to the server database schema, which is generally a bad thing. A better solution is to just restore the database to the pre-test state (optionally saving the state post-test for debugging), although that can be either slow (for non-trivial db) or a problem (if the server can’t tolerate it).

Cleanup - codebase

features/steps/login_steps.rblink
25
26
27
28
29
30
31
32
33
34
35
36
37
38
When(/^I create an account and character$/) do
  @connection.cmd({"String" => "", "Match" => /Login\: /}) { |c| puts "0 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "new", "Match" => /account\: /}) { |c| puts "1 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_name], "Match" => /address\: /}) { |c| puts "2 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /address\: /}) { |c| puts "3 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /12\)\: /}) { |c| puts "4 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /password\: /}) { |c| puts "5 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /Gender\: /}) { |c| puts "6 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_gender], "Match" => /a Race\: /}) { |c| puts "7 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_race], "Match" => /Class\: /}) { |c| puts "8 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_class], "Match" => /\(y,n\)\: /}) { |c| puts "9 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "n", "Match" => /character\: /}) { |c| puts "10 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_name], "Match" => /Command\: /}) { |c| puts "11 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
end

Well, that’s getting kind of ugly. Let’s do some refactoring. A few helper methods would be good:

features/lib/misc_helper.rblink
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
def telnet_commands(connection, commands)
  commands.each do |command|
    output = telnet_command(connection, command[0], command[1])
  end
  output
end

def telnet_command(connection, string, match, overrides = {})
  puts "Sending #{string} and waiting for #{match}..."
  connection.cmd({"String" => string, "Match" => match}.merge(overrides)) { |c| puts ansi_strip(c) unless c.nil? }
end

def ansi_strip(string)
  parsed = string.gsub(/\e\[[^a-zA-Z]*[a-zA-Z]/, "")
  # puts "I made #{string} into #{parsed}!"
end

So now I can change the step to:

features/steps/login_steps.rblink
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
When(/^I create an account and character$/) do
  telnet_command(@connection, "", /Login\: /, {"Timeout" => 120})
  telnet_commands(@connection, [["new", /account\: /],
                                [@user[:acct_name], /address\: /],
                                [@user[:acct_email], /address\: /],
                                [@user[:acct_email], /12\)\: /],
                                [@user[:acct_password], /password\: /],
                                [@user[:acct_password], /Gender\: /],
                                [@user[:char_gender], /a Race\: /],
                                [@user[:char_race], /Class\: /],
                                [@user[:char_class], /\(y,n\)\: /],
                                ["n", /character\: /],
                                [@user[:char_name], /Command\: /]
                               ])
end

which seems a bit more manageable. Let the refactoring commence!

Wait, what?

So I keep refactoring, doing stuff like this:

features/steps/login_steps.rblink
65
66
67
68
69
70
71
72
73
When(/^I log in with the wrong then right password$/) do
  @connection = create_telnet_connection
  telnet_command(@connection, "", /Login\: /, {"Timeout" => 120})
  output = telnet_commands(@connection, [[@user[:acct_name], /Password\: /],
                                         ["oops", /:/]])
  expect(output).to include("Invalid password.")
  telnet_commands(@connection, [[@user[:acct_name], /Password\: /],
                                [@user[:acct_password], /Command\: /]])
end

and I get this:

Sending oops and waiting for /:/...
Invalid password.
login:
expected output to some stream to include "Invalid password.", but it does not respond to `include?`
Diff:
@@ -1,2 +1,2 @@
-["Invalid password."]
+(output to some stream)
 (RSpec::Expectations::ExpectationNotMetError)
./features/steps/login_steps.rb:70:in `/^I log in with the wrong then right password$/'
features/login.feature:25:in `And I log in with the wrong then right password'

What the heck is “output to some stream”? Maybe what’s coming back from Net::Telnet#cmd isn’t quite what I expected. I’ll add a .to_s to the return from telnet_commands to sort that out, and:

expected "#<RSpec::Matchers::BuiltIn::Output:0x3114558>" to include "Invalid password." (RSpec::Expectations::ExpectationNotMetError)
./features/steps/login_steps.rb:70:in `/^I log in with the wrong then right password$/'

Oh? Oh. Rspec 3 has a built-in matcher called output. I’ll have to remember that in future and name things better.


Clarification

Just to be clear, that “too sick to go to work but not too sick to make games” thing really never happens. I’m a software engineer with a decent internet connection; if I can do gamedev I can do my job. So it waits until after hours, sick or no.


Day 38 code - tests Tool - TinyTDS