Table of contents
Let's continue on my to-do list in Ruby. Here are the focuses in part 2:
Methods in
Todo
classUnit tests for every method in
Todo
Before going on...
There are 2 files we need to add to the directory.
.ignore
: adddata.csv
to the file so it wouldn't be pushed to GitHubdata.csv
: we need a place to store to-dos so we create a CSV file to do so by runningtouch data.csv
Preparing RSpec tests
By following TTD, we should prepare our tests before diving into coding.
In part 1, we decided to have 4 methods. They are .list
, .edit
, .add
and .delete
. Let's set up some tests for each of them. But first, we should think about the input and output of each method.
What do we expect from the methods?
.list
: should list out all the to-dos in the terminal. No argument is needed for this method..edit
: should take a to-do ID (Int
) and the new content (String
) as arguments and replace the specific to-do..add
: should take aString
as an argument and generate a to-do ID by default.delete
: should take a to-do ID (Int
) as an argument and delete the specific to-do
What should we test?
should receive a message when everything is running as expected
should receive an error when something is wrong
By following these 2 directions, we can create a plan for each test.
Planning for the tests
.list
:
- should display a correct message when the list is empty
.edit
:
should update the selected todo with the new content
should throw an error when the user enters a wrong to-do ID
should throw an error when the user enters empty content
.add
:
should create a new to-do
should throw an error when the user enters an invalid argument
.delete
:
should delete a to-do
should throw an error when the user enters a wrong to-do ID
should throw an error when the user enters an empty string
Execute the plan
We can now transform these lines into RSpec
code and put them into spec/todo_spec.rb
.
But before that, we should think about this:
Before all tests, a new
Todo
and also a new CSV file for testing should be created. We shouldn't use thedata.csv
for testing in case there are already some data in it.After all tests, we should delete the new testing CSV file.
Here is the code to do so:
describe Todo do
before (:all) do
File.new("data_test.csv", "w")
@Todo = Todo.new("data_test.csv")
end
after (:all) do
File.delete("data_test.csv")
end
end
For the rest of the code, I don't show them here one by one here. You can check this link for the whole file.
Initialising Class Todo
I decided to give flexibility to the user to change the file name data.csv
, so this initialize
method will take a file name as an argument but with a default input.
It will be saved in an instance variable called @file_name
so that I can access it any time I want within this object.
def initialize(file_name = "data.csv")
@file_name = file_name
end
Methods in Todo
I took CRUD
(create, read, update and delete) methodology as the blueprint for this Todo
object.
.list
This
READ
method should print out all to-dos fromdata.csv
in a format like this:
1. This is the 1st to-do
2. This is the 2nd to-do
3. This is the 3rd to-do
What should this method do?
check if the list is empty. If the answer is yes, print something to notify the user.
access
data.csv
and loop through all the lines insideprint out each of them with the above format
def list
if !load_todos.empty?
load_todos.each_with_index do |(key,value),index|
puts "#{index + 1}. #{value}"
end
else
puts "The list is empty."
end
end
each_with_index
is a perfect solution here when I want to have value
and index
at the same time. As index
will start from 0, I have to add 1 to the index
.
You might notice that I have already decided to save my to-dos in a hash. I think a hash is a more appropriate way to save data instead of an array.
.add
This
CREATE
method should take aString
and add it to the to-do list
What should this method do?
check if the input is empty or invalid. If the answer is yes, print something to notify the user.
load all todos and save them to a local variable named
list
add a new item to
list
save the new
list
todata.cvs
You might notice load_todos
and save_todos
are new here. As I decided to separate them from these 4 methods in order to keep it DRY
as we have to do it in most of the methods in this object.
I will explain the details of these 2 methods in the next section.
def add(content)
if content && content != ""
list = load_todos
# Check if the list is empty
if list.length > 0
list[(list.to_a.last[0] + 1)] = content
else
list[1] = content
end
save_todos(list)
else
puts "Invalid input. Please enter again."
end
end
.edit
This
UPDATE
method should take a todo ID (Int
) and a new content (String
) then replace the old content of the todo with the new one
What should this method do?
load all todos and save them to a local variable named
list
check if the todo ID exists in the list. Display an error message if not.
check if the user input is correct. Display an error message if not.
replace the content with the user input
save the new
list
todata.csv
def edit(todo_id, content)
list = load_todos
if list.has_key?(todo_id)
if content && content != ""
list[todo_id] = content
save_todos(list)
else
puts "Invalid input. Please enter again."
end
else
puts "To-do is not found. Please enter again."
end
end
.delete
This
DELETE
method should take a todo ID (Int
) and delete the todo with the same id
What should this method do?
load all todos and save them to a local variable named
list
We should check if the todo ID exists in the list. Display an error message if not.
filter out the specific todo from
list
save the new
list
todata.csv
def delete(todo_id)
list = load_todos
if list.has_key?(todo_id)
list.delete_if do |key, value|
key == todo_id
end
save_todos(list)
else
puts "To-do is not found. Please enter again."
end
end
Dealing with repetitive codes
You might notice that the following actions will be executed for most of our methods:
loading todos and saving them in
list
save the new
list
todata.csv
By following DRY
principle, we should separate them from our existing methods and make their own.
Moreover, I don't think the user should touch these 2 methods so I make them as private
. What they do is just simply READ
and WRITE
the data.csv
. Here are the lines:
private
# Take an array and write each of its item into a CSV
def save_todos(list)
CSV.open(@file_name, "wb") do |csv|
list.each do |key, value|
csv << [key, value]
end
end
end
# Read data from a CSV and return an array
def load_todos
list = {}
CSV.foreach(@file_name) do |line|
id, content = line
list[id.to_i] = content
end
list
end
And this is the end of part 2. You can now run rspec
and see if there is any problem. The to-do list should now be ready to launch online. I will leave the rest to part 3.
What will we do in part 3?
Why did I change from TravisCI to CircleCI?
As a self-learner, I found it difficult to find learning resources for TravisCI. Compared to CircleCI, the latter is so much easier to understand in terms of documentation and tutorials. As a developer, I will always pick the right tool for myself to maximise my work efficiency. And I think CircleCI is the right one for this project.