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-2022 Amp Books LLC All Rights Reserved