The main purposes of this Ruby project:
- Encapsulate all my practical knowledge of Ruby into a Living Document of RSpec unit tests and Cucumber Features.
- Refine my TDD, BDD, and SBE-focused approach to solving problems.
- Practise vital programming skills -- Git, information security, etc.
In short, it goes some way to answering the question, "What problems can you solve, and how do you solve them in Ruby"?
- Install Ruby 2.6.4
- Clone this repository: https://github.com/clockworkpc/cpc-ruby.git
- Navigate to the root directory of project.
- Run
bundle install
in the command line to install the project gems. - To run the tests you have the following options:
bundle exec rspec
to run all the RSpec unit tests;bundle exec cucumber
to run all the RSpec unit tests;bundle exec guard
to run Guard in the background, which will run the RSpecs or Cucumber tests whenever a file is changed or saved, if you want to run a specific test,bundle exec rspec {_spec.rb filepath}
to execute a specific RSpec file.bundle exec cucumber {_.feature filepath}
to execute a specific Cucumber feature file.
This functionality requires the following:
-
A paid subscription to https://isbndb.com.
-
An authenticated API Key stored in the
.env
file:ISBN_DB_API_KEY={your API key}
Accordingly, Specs that require the above are ignored by the default scope group in the Guardfile
.
The Guardfile
has the following scope groups:
:offline
=> runs Specs taggedoffline: true
. (Default):online
=> runs Specs taggedonline: true
.
The Guardfile
does not track Cucumber features, even though it could, because features should be tested by specific events, not merely by a change to the file. This way, active development is done through TDD in RSpec, and when all the Specs pass, then I go back to the Feature. Without this strict separation of concerns, RSpec and Cucumber will do each other's job.
The following Rake tasks are available if you want to run Specs and Features, with HTML reports:
$ bundle exec rake -T | grep -i "test"
rake test:all_tests # Execute all Specs and Cucumber Features
rake test:offline_features # Execute Cucumber features tagged @offline
rake test:offline_specs # Execute all Specs tagged @offline, output to terminal
rake test:offline_specs_html # Execute all Specs tagged @offline, output to HTML
rake test:offline_tests # Execute all Specs and Cucumber Features tagged @offline
rake test:online_features # Execute Cucumber features tagged @online
rake test:online_specs # Execute all Specs tagged @online, output to terminal
rake test:online_specs_html # Execute all Specs tagged @online, output to HTML
rake test:online_tests # Execute all Specs and Cucumber Features tagged @online
- DRY Ruby code in
lib
- Test-driven development (TDD) provided with RSpec unit tests in
spec
, - Behaviour-driven development (BDD) with Cucumber Feature and Step files in
feature
. - Proper namespacing and inheritance (as can be seen from
lib/cpc
and its child directories) - Documentation (i.e. this
README
) - Management of dependencies with the
Gemfile
- Handling of events upon file system modifications with
guard
- Create
.feature
file in thefeatures
folder, wherein to define the problem and desired output in plain English [1], containing a large number of scenarios. - Run
cucumber
in the terminal and get the missing Steps - Create a
*_steps.rb
file in thefeature/step_definitions
folder and define how the Feature inputs will be passed to the methods. - Create a
*_spec.rb
file in thespec
folder, and add as inputs and expectations a sample of the those in the from the Cucumber Feature. - Create a
.rb
file in thelib
folder: - Class, if the functionality requires instantiation or to keep track of state.
- Module if the problem can be solved with static methods.
- Apply TDD (Red-Green) until the RSpecs are satisfied.
- Run
cucumber
on all the scenarios and make adjustments to the Ruby code or the RSpecs until all the scenarios in the Feature are satisfied.
[1] Gherkin tends to become more pseudo-code than English in real life, but it's a great tool for putting the big picture into words. (And besides, that's what a lot of companies work with)
There are three main namespaces:
Cpc::Util
Cpc::Toolkit
Cpc::Api
Cpc::Util
contains Modules whose methods are abstractions of generally useful things that I use in my day-to-day coding.
Cpc::Toolkit
contains Classes that have the option of include
-ing Cpc::Util
Modules. Each Class contains the functionality needed to solve a particular real-life problem.
Cpc::Api
contains Classes that invoke the custom API wrapper Cpc::Util::ApiUtil
.
As a further demonstration, there is a fourth namespace: Cpc::Codewars
, which contains solutions to a few Codewars puzzles. These are either Modules or Classes, as the above-described considerations require.
More for discipline than expedience, wherever possible, I try to write methods in Modules rather than Classes, in order to clarify whether my methods really need to keep track of state. A Module tends to house the most abstracted functionality, e.g. Cpc::Util::ApiUtil, whereas a Class contain application-specific attributes and methods, e.g. Cpc::Api::GoogleSheets.
To quote the excellent answer on Stack Overflow:
A class should be used for functionality that will require instantiation or that needs to keep track of state. A module can be used either as a way to mix functionality into multiple classes, or as a way to provide one-off features that don't need to be instantiated or to keep track of state. A class method could also be used for the latter.
With that in mind, I think the distinction lies in whether or not you really need a class. A class method seems more appropriate when you have an existing class that needs some singleton functionality. If what you're making consists only of singleton methods, it makes more sense to implement it as a module and access it through the module directly.