Ruby Project Etiquette: How to Mind Your P’s and Q’s in a Ruby Project
In this session we’re going to go over some common best practices for organizing and managing code in our Ruby projects. By the end of the lesson, you should be comfortable with the following tasks.
- File naming conventions
- Directory structure conventions
- Difference between
- How to build a rakefile and why you’d want to
Slides available here
- rake task
- How have you been organizing your projects so far?
- What are the advantages of following conventions in project organization?
- Do you tend to use require or require_relative? How does the pathing work?
Directory and File Organization
File/Class Naming Conventions
- Snake-case file names (
- End files in
- Classes are named using PascalCase a.k.a. UpperCamelCase
- Match file names to the class name – e.g. a file containing the class
In a standard Ruby project, we tend to organize code into 4 subdirectories:
libfor source code
testfor test files
datafor data-related files (.txt, .csv, etc)
binfor any “executable” files (you may not have encountered any of these yet; if you don’t have them, leave
Additionally, it’s common for test files and source files to match relatively 1-to-1. Thus a class called
RotationGenerator will generally be found in the source file at
lib/rotation_generator.rb and will have a corresponding test file at
Take the following 2 code snippets and place them into a correct ruby project structure. Consider:
- What directories and files do you create
- What content goes into each file
- How do you require the code file from the test
class Hello def greet “Hello, World!” end end
require "minitest/autorun" # _______________ <--- Your Require Statement Here class HelloTest < Minitest::Test def test_it_greets hello = Hello.new assert_equal “Hello, World!”, hello.greet end end
Require statements often trip us up, but there are some straightforward guidelines we can follow that make things much more reliable.
Here’s a quick overview of how
require_relative attempts to require a second file using a path relative to the file that is requiring it.
- Does NOT matter where you run the test from (searches for path relative to the file the requirement is in)
- As directory structure gets more complex, navigating relative to the file you require come can become convoluted (
require attempts to require a second file relative to the place from which the first file is being run – that is, relative to your present working directory when you type
- DOES matter where you run the test from
- require tends to behave more consistently in complex scenarios and project structures (
- require is also what we’ll use for external gems and libraries. This is because…
- require is designed to cooperate with ruby’s $LOAD_PATH
- Rails assumes we’re running from the main project directory.
Err on the Side of
Consider a project with the following structure.
. ├── lib │ ├── enigma.rb └── test ├── enigma_test.rb
Generally within the Ruby community it is assumed that you will be running your test files from the
project/ directory and not from within the
Avoid the temptation to navigate into go into test or lib directories through terminal to run code (i.e.
test$ ruby enigma_test). Use
enigma$ ruby test/enigma_test.rb instead.
Why do we prefer
Assuming the directory structure above,
enigma_test.rb could include either of the following lines (remember you can leave
.rb off when you are requiring a file):
If you are running your test files from within the
project directory, both of these will work the same. If you move down into the
project/test directory, the first would then be unable to find the
enigma.rb file. So why would we use something that might break depending on where we execute our code? Well, there are tradeoffs.
What seems more brittle in this case is likely actually more resilient to future changes. Remember the example above: if our application and test suite grow, we may decide that we want to include subdirectories for our tests. If we use
require_relative that means that we have to add a
../ to each and every one of our tests. If we use
require we can simply move our files to a new subdirectory and continue to run our tests from the
project directory as we have been doing.
Additionally, using require tends to be more common within the community. Programmers get worked up about weird things and sometimes it’s best to just go with the flow.
Check for Understanding
Set up the following code in
test/hello_test.rb files. Experiment with which path formats you can get working in each scenario in the table below and record the path there.
class Hello def greet “Hello, World!” end end
require "minitest/autorun" class HelloTest < Minitest::Test def test_it_greets hello = Hello.new assert_equal “Hello, World!”, hello.greet end end
|require type||running file from project directory||running file from test directory|
Rakefiles and Test Runners
Rake tasks come from make tasks, Unix origins. They are used for task management and building projects. For a C project, “building” means compiling and verifying that things work. Ruby projects don’t get compiled, so what does “building” mean for a Ruby project?
Rake tries to create a standardized solution for this problem so that you can interact with build and program prep the same no matter which application you’re running. Not only does it give you set commands, but you can also build your own tasks and run them through Rake.
By default Rake will look for a file name
Rakefile in the root of your project. You’ll define your RakeTasks in here.
Using Rake to Build a Task
A task is comprised of the keyword
task followed by the name you assign to the task written as a symbol (
:task_name). This gets passed a block of code (
do ...end - similar to how we pass a block of code to the
Let’s build your first Rake task!
Using the directory structure listed above, create a project named
party_planner. Make sure to include a Rakefile.
# Rakefile desc "a rake welcome message" task :welcome do puts "Welcome to Rake tasks!" end
This task would then be run from the command line using
rake welcome (from the project root – noticing a pattern?) We can also run
rake -T to see which Rake commands we’ve defined and what they do.
Now create an
Event class with an instance method of
greeting that puts “Welcome to the party!”. Create a second Rake task that calls this method.
Building a Test Task
Let’s build a task that will run all our tests. Our objective is to be able to go into the root directory of your project, type
rake test, and run our test suite (all of your tests) with that one command. You’ll need to use
* to pull in files of various names but still end in
desc "run all tests" task :test do ruby "test/*_test.rb" end
So here’s the thing, that code up there only runs one thing. What do we have to do to get all of it to run?
my_files = FileList['test/**/*.rb'] my_files.each do |file| ruby file end
Look at that! We are getting a list of all of the files in our test directory, and we are enumerating over the list of files and then we are just going to run each one.
This is great, but we are missing something. Each runs individually, and we have to scroll up a lot. This is what we would call less than ideal. How do we get everything to run altogether? As if it was one big test file?
Have your Rakefile look like this.
require 'rake/testtask' Rake::TestTask.new do |t| t.pattern = "test/**/*_test.rb" end
testtask is a thing built into Rake that will do that for us, we just need to follow the format above.
If you run the command
rake without any further arguments, Rake will automatically look for a task named
default. We can also set up a Rake task to have prerequisites - that is other tasks that must be run before the current task is run. If you set a prerequisite, Rake will automatcially run those other tasks first, you don’t even have to worry about it. Using this knowledge, we can add the following to our Rakefile:
task default: ["test", "welcome"]
This will run both our
welcome tasks when we run `rake, but not the third task you created independently.
Update your current project to follow these conventions.
- How does require_relative work?
- How does require work?
- Which does Ruby convention prefer?
- What is a Rake Task? Why would you use one?
- What are the components of a Rake Task?
Update at least one previous project (Credit Check, Jungle Beat, Date Night, Night Writer, Complete Me) to also follow these conventions.