Testing PostgreSQL scripts with RSpec and pg_tester
Neeran Gul
At Yammer, we run more than a dozen different PostgreSQL clusters supporting our Production applications. As with most databases, we run regular maintenance tasks such as rotating user credentials, adding new users and databases, dealing with index bloat reduction, and so on. To make life easier, we have created additional tools to streamline our maintenance tasks without human intervention. The purpose of this article is to show how we use our pg_tester gem with RSpec to provide a TDD-approach for writing maintainable scripts and automation involving PostgreSQL.
The Ruby gem pg_tester provides users with the functionality to write tests for their scripts involving PostgreSQL. It creates a temporary database in a directory of the user’s choice and allows it to be accessible via the pg gem API. It was designed to facilitate automated tests against a PostgreSQL instance reliably and cleanly, and without needing to do any hardcore database configuration and setup! You only need to have installed PostgreSQL locally first via whichever method you prefer (dpkg, rpm, tar.gz, exe, source etc).
The pg_tester gem is available from Microsoft’s repository on Github: https://github.com/Microsoft/pgtester
As an example, we’ll create a new user by taking the TDD-approach and using pg_tester. We will also use ruby 2.1.2 (available at https://rvm.io/).
Here’s what our final test will perform:
1. Create a new user with a username and password.
2. Verify the new user exists in the database.
Let’s setup our dev environment. The directory setup and layout looks like this:
pg_tester_example
├── Gemfile
└── spec
├── postgresql_manager.rb
└── postgresql_manager_spec.rb
Our Gemfile is as follows:
### Gemfile
source "https://rubygems.org"gem "pg_tester"
gem "pg"
gem "rspec"
After the Gemfile is in place, running a bundle install will install the necessary dependencies.
With those quick two steps, setup is complete! Now we can look at the code:
Here, we are writing a spec for our PostgresqlManager class. We want to create DummyUsername user in our temporary database with the create_new_user method. Let’s put in an empty class with some basic parameters, so we don’t get errors.
### postgresql_manager.rb ###
class PostgresqlManager
def initialize(host, database, user, port)
end
end
Here is our spec:
### postgresql_manager_spec.rb ###
require "pg_tester"
require "postgresql_manager"describe PostgresqlManager do
## create a local test DB
psql = PgTester.new({
database: "testbuddy",
user: "buddy",
}) before(:each) do
psql.setup
end after(:each) do
psql.teardown
end subject { described_class.new(psql.host, psql.database, psql.user, psql.port) } describe "create_new_user" do
context "testing create user method" do it "should create a new user" do
subject.create_new_user("DummyUsername", "complexPassword")
result = psql.exec "SELECT usename FROM pg_catalog.pg_user"
expect(result.values).to include(["DummyUsername"])
end end
end
end
In the above code sample, we create a PgTester instance, then create our subject PostgresqlManager class. In this example, we want to run psql.setup before each test in the context block. That will spin up our temporary database and tear it down afterwards by running psql.teardown. We run our create_new_user method then verify the user is created in PostgreSQL.
We can run the above by doing bundle exec rspec.
$ bundle exec rspec
FFailures:1) PostgresqlManager create_new_user testing create user method should create a new user
Failure/Error: subject.create_new_user('DummyUsername', 'complexPassword')NoMethodError:
undefined method 'create_new_user' for #<PostgresqlManager:0x007fcf3313eae8>
# ./spec/postgresql_manager_spec.rb:20:in 'block (4 levels) in <top (required)>'Finished in 4.72 seconds (files took 0.50818 seconds to load)
1 example, 1 failureFailed examples:rspec ./spec/postgresql_manager_spec.rb:19 # PostgresqlManager create_new_user testing create user method should create a new user
This will give us a failing test, citing that the create_new_user method does not exist. Let’s add a create_new_user method to our class.
### postgresql_manager.rb ###
require "pg"class PostgresqlManager
def initialize(host, database, user, port)
@connection = PG::Connection.open(:host => host, :dbname => database, :user => user, :port => port)
end def create_new_user(username, password)
results = @connection.exec "CREATE ROLE #{username} WITH password '#{password}' LOGIN"
@connection.close()
end
end
We open a connection to our database via the pg gem in the initialize method whilst passing in arguments. This class is generic, so we can pass any database connection details. We are going to pass in our temporary database connection details for purposes of our test. The create_new_user method will issue a CREATE ROLE statement to create a new user with supplied credentials and LOGIN permissions.We then close our connection to the database.
Let’s run our tests now:
$ bundle exec rspec
.Finished in 4.64 seconds (files took 0.49654 seconds to load)
1 example, 0 failures
Hooray! Our tests pass!
We’re really happy to release pg_tester to the Open Source community on the Microsoft Github. More examples and installation instructions can be found here:
https://github.com/Microsoft/pgtester
Neeran Gul is an engineer on the Production Engineering Team in the Yammer London Office and author of the pg_tester gem