TinyTDS Testing with Rspec Mocks in Rails
Edu Depetris
- Jun 23, 2024- R Spec
- Tiny Tds
- Microsoft Sql
- Testing
- Ruby On Rails
In our Ruby on Rails project, we are using RSpec as the test framework and facing the challenge of connecting to external Microsoft SQL Servers to read and write information. For this connection, we’re using the TinyTDS gem, which leverages FreeTDS’s DB-Library.
The TinyTDS API is simple and consists of these classes:
- TinyTds::Client - Your connection to the database.
- TinyTds::Result - Returned from issuing an #execute on the connection. It includes Enumerable.
- TinyTds::Error - A wrapper for all FreeTDS exceptions.
Here’s an example of how this gem works:
client = TinyTds::Client.new(username: 'sa', password: 'secret', host: 'mydb.host.net')
# TinyTds::Result
results = client.execute("SELECT c.CustomerID FROM SalesLT.Customer")
results.each do |row|
puts row
end
Now, let’s consider a hypothetical service that we want to test. This service connects to a Microsoft SQL server via TinyTDS and then performs an action with the result. In production, the service will use a TinyTDS wrapper.
# app/services/tiny_tds_example.rb
class TinyTdsExample
def call
client = TinyTds::Client.new(username: 'sa', password: 'secret', host: 'mydb.host.net')
# TinyTds::Result
results = client.execute("SELECT c.CustomerID FROM SalesLT.Customer")
results.each do |row|
create_customer(row)
end
end
end
Our goal here is to stub the TinyTds::Result and validate the queries that we send to the server.
Let’s take inspiration from the WebMock gem on how stubbing will look:
Here’s a simple example of how WebMock works:
stub_request(:post, "www.example.com/api/search").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return({data: "abc"})
uri = URI.parse("http://www.example.com/")
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) do |http|
http.request(req, "abc")
end # ===> Success
We’ll create a similar method to stub_request that allows us to take an SQL query and then return the result that we want from the Microsoft SQL server. Our method will look like this:
stub_db_call(query:, response:)
Let’s create the structure of the test with RSpec:
require "rails_helper"
RSpec.describe TinyTdsExample, type: :service do
let(:service) { described_class.new }
describe "#call" do
it "do something" do
# Our mock here
expect { service.call }.to change { Customer.count }.by(2)
end
end
end
sql_query = ""
response = []
fake_client = instance_double(TinyTds::Client)
fake_result = instance_double(TinyTds::Result)
allow(TinyTds::Client).to receive(:new).and_return(fake_client)
# Here we validate the query
expect(fake_client).to receive(:execute).with(query).and_return(fake_result)
# Here we stub the response
allow(fake_result).to receive(:each).and_return(response)
This works!
Let’s take one more step and move this into a method that we can reuse:
# spec/support/tiny_tds_db_stub.rb
module TinyTdsDbStub
def stub_db_call(query:, response:)
fake_client = instance_double(TinyTds::Client)
fake_result = instance_double(TinyTds::Result)
allow(TinyTds::Client).to receive(:new).and_return(fake_client)
expect(fake_client).to receive(:execute).with(query).and_return(fake_result)
allow(fake_result).to receive(:each).and_return(response).once
end
end
Now let’s use this new method and clean up the test file a bit:
require "rails_helper"
RSpec.describe TinyTdsExample, type: :service do
let(:service) { described_class.new }
describe "#call" do
it "do something" do
sql_query = %{
SELECT c.CustomerID FROM SalesLT.Customer
}.squish
response = [
{ CustomerID: 123 },
{ CustomerID: 456 }
]
stub_db_call(query: sql_query, response: response)
expect { service.call }.to change { Customer.count }.by(2)
end
end
end
Happy coding!