This exercise covers the following topics.
bin/sinatra.
From a configuration management perspective,
what problems do you see with the MongoDB setup?
The host, port, and database names are hardcoded. Changing their values requires modifying source code.
YAML is a human-readable data serialization format. It serves the same purpose as XML but is much less tedious to write. It stands for "YAML Ain't Markup Language," which emphasizes its focus on data serialization, not document markup.
SettingsLogic is a RubyGem that reads a YAML file containing configuration settings.
Add settingslogic as a runtime dependency to statowl.gemspec.
In your bin/sinatra file, create a new class
to handle your settings and point it to the location
of the YAML configuration file that you will create next:
... require 'settingslogic' class Settings < Settingslogic source Dir.pwd + '/application.yml' end ...
Insert the MongoDB settings into a new YAML file application.yml
in your project root directory:
mongo_server: 'localhost' mongo_port: '27017' mongo_database: 'statowl_database'
Finally, remove the hardcoded values in bin/sinatra and replace them with
the constants specified in application.yml:
...
def connect_to_database
config = Settings.new
mongo_URL = 'mongodb://' + config.mongo_server + ':' + config.mongo_port + '/' + config.mongo_database
mongo_client = Mongo::Client.new(mongo_URL)
collection = mongo_client[:statowl_stored_numbers]
repository = Statowl::MongoRepository.new(collection)
yield repository
end
...
Run Cucumber - still green? You now have a central location for paths, names, URLs, ports, and any other configuration settings you project needs.
Large projects often have complex Rake tasks.
Cleaning out a MongoDB database, for example,
is more complex than cleaning out a file directory.
Extracting a task file like this from the Rakefile reduces
clutter and makes the task independently testable.
Create a new unit test file spec/mongo_clean_spec.rb and insert the following into it:
require 'settingslogic'
require_relative '../lib/statowl'
require_relative '../lib/statowl/mongo_cleaner'
class Settings < Settingslogic
source Dir.pwd + '/application.yml'
end
describe 'A MongoDB database cleaner' do
def empty?(collection)
collection.find({}).count == 0
end
it 'should clean the database' do
config = Settings.new
mongo_URL = 'mongodb://' + config.mongo_server + ':' + config.mongo_port + '/' + config.mongo_database
mongo_client = Mongo::Client.new(mongo_URL)
collection = mongo_client[:statowl_stored_numbers]
collection.insert_one({ "key" => "my_key", "value" => "my_value" })
expect(empty?(collection)).to be(false)
cleaner = Statowl::MongoCleaner.new(collection)
cleaner.clean
expect(empty?(collection)).to be(true)
end
end
Execute rake spec to check that it fails and then
create a new file lib/statowl/mongo_cleaner.rb to make it pass:
module Statowl
class MongoCleaner
def initialize(collection)
@collection = collection
end
def clean
@collection.find({}).delete_many
puts 'Mongo: All Clean!'
end
end
end
It's time to turn your well tested database cleaner into a Rake task.
Add the new file to lib/statowl.rb:
require 'statowl/mongo_cleaner'
Add the new task to Rakefile:
require 'statowl'
require 'settingslogic'
require 'mongo'
...
class Settings < Settingslogic
source Dir.pwd + '/application.yml'
end
...
namespace 'mongo' do
task :clean do
config = Settings.new
mongo_URL = 'mongodb://' + config.mongo_server + ':' + config.mongo_port + '/' + config.mongo_database
mongo_client = Mongo::Client.new(mongo_URL)
collection = mongo_client[:statowl_stored_numbers]
Statowl::MongoCleaner.new(collection).clean
end
end
...
It has a mongo namespace to differentiate
it from Rake's build-in clean task, so execute the new task like this:
rake mongo:clean
To automatically clean the database before running RSpec tests you need to make the spec
task dependent on the mongo:clean task.
The syntax is a little different than other tasks in your Rakefile because
the mongo:clean task is namespaced.
In Rakefile add
task :spec => "mongo:clean"
Now you start each test run with an empty collection.
Execute rake spec and you will observe "Mongo: All Clean!"
at the top of the console output just before the tests are run.
Unit tests are usually for new production code that you write, not for dependencies like external databases and web services. Theoretically, if the dependencies work correctly and your code works correctly then they should all work together correctly. Integration tests prove that this assumption matches reality.
The use of test doubles for external dependencies enables unit tests to be run in isolation. Here are some of the reasons why they are used:
There are three general types of test doubles based on their sophistication:
The database operations for your StatOwl application have a critically important external dependency: MongoDB. In real life it might be running on a remote server. The extent of your tests and the slowness of the database might be causing your tests to run slowly. This could be an opportunity to create a test double.
All of your database operations rely on low-level methods of the MongoDB::Collection
class: insert, find, and remove.
This is an ideal boundary for a test double that allows unit tests to run in isolation from
the MongoDB database.
There are popular libraries for writing Ruby test doubles, but the easiest implementation is
a simple class.
To the top of spec/mongo_spec.rb insert a test for a new
fake of a collection that you can pass to Statowl::Mongo in lieu of a real
MongoDB collection:
require_relative '../lib/statowl/mongo_repository'
#
# ToDo:
# A new Statowl::Collection class will go here.
#
describe 'a fake collection' do
it 'should implement insert_one, find, and delete_many' do
collection = Statowl::Collection.new
expect(collection.find({}).size).to eq(0)
document = { "key" => "key1", "value" => "value1" }
collection.insert_one document
documents = collection.find({ "key" => "key1" })
expect(documents.any?).to eq(true)
expect(documents.first).to eq(document)
document = ({ "key" => "key2", "value" => "value2" })
collection.insert_one document
documents = collection.find({ "key" => "key2" })
expect(documents.any?).to eq(true)
expect(documents.first).to eq(document)
#delete the first record
collection.delete_many({ "key" => "key1" })
documents = collection.find({ "key" => "key1" })
expect(documents.any?).to eq(false)
documents = collection.find({ "key" => "key2" })
expect(documents.any?).to eq(true)
expect(documents.first["value"]).to eq("value2")
end
end
...
Make sure that it fails.
Then make it pass by replacing your ToDo comments with an implementation
of the Statowl::Collection class:
...
class Statowl::Documents < Array
def any?
self.size > 0
end
def first
self[0]
end
end
class Statowl::Collection
def initialize
@documents = Statowl::Documents.new
end
def insert_one(document)
@documents << document
end
def find(query)
results = @documents.select do |hash|
query.empty? || hash['key'] == query['key']
end
Statowl::Documents.new(results)
end
def delete_many(query)
trash_docs = find(query)
@documents.delete(trash_docs.first)
end
end
...
Now you can use your fake collection to run your RSpec tests in spec/mongo_spec.rb
without MongoDB.
... describe 'A repository for numbers' domongo_client = Mongo::Client.new("mongodb://localhost:27017/statowl_database") collection = mongo_client[:statowl_stored_numbers] repository = Statowl::MongoRepository.new(collection)repository = Statowl::MongoRepository.new(Statowl::Collection.new) ...
In this exercise you used SettingsLogic to manage your RubyGem's configuration settings.
SettingsLogic needs only a few lines of code to declare the Settings class
and point it to a YAML file containing configuration settings.
You also organized a complex Rake task by defining a separate class for the details. Simplifying Rake tasks through delegation is a technique often used by well-written gems.
Finally, you wrote a test double to unit test your production code without connecting to MongoDB. There are three types of test doubles: a simple stub with hardcoded values, a more sophisticated fake that represents an alternative implementation of the interface, and a highly sophisticated mock that verifies expected collaboration with other objects.
Copyright © 2005-2026 Amp Books LLC All Rights Reserved