Day 08 - Ready for Challenge! To-do List by Ruby (Part 2)

Let's continue on my to-do list in Ruby. Here are the focuses in part 2:

  • Methods in Todo class

  • Unit tests for every method in Todo

Before going on...

There are 2 files we need to add to the directory.

  1. .ignore : add data.csv to the file so it wouldn't be pushed to GitHub

  2. data.csv : we need a place to store to-dos so we create a CSV file to do so by running touch 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 a String 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:

  1. Before all tests, a new Todo and also a new CSV file for testing should be created. We shouldn't use the data.csv for testing in case there are already some data in it.

  2. 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 from data.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?

  1. check if the list is empty. If the answer is yes, print something to notify the user.

  2. access data.csv and loop through all the lines inside

  3. print 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 a String and add it to the to-do list

What should this method do?

  1. check if the input is empty or invalid. If the answer is yes, print something to notify the user.

  2. load all todos and save them to a local variable named list

  3. add a new item to list

  4. save the new list to data.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?

  1. load all todos and save them to a local variable named list

  2. check if the todo ID exists in the list. Display an error message if not.

  3. check if the user input is correct. Display an error message if not.

  4. replace the content with the user input

  5. save the new list to data.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?

  1. load all todos and save them to a local variable named list

  2. We should check if the todo ID exists in the list. Display an error message if not.

  3. filter out the specific todo from list

  4. save the new list to data.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 to data.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.

Did you find this article valuable?

Support Terry Cheng by becoming a sponsor. Any amount is appreciated!