Learn Ruby with TDD
  • Learn Ruby with TDD
  • Ruby tooling
  • Object-Oriented Design
  • Fundamentals
    • Hello, world
    • Objects, Methods & Integers
    • Loops, Blocks & Strings
    • Arrays & Floats
  • Meta
    • Bibliography
Powered by GitBook
On this page
  • Text Banner
  • Write the test first
  • Write the minimal amount of code for the test to run
  • Write enough code to make the test pass
  • Refactor
  • Source Control
  • A bug report arrives
  • Write the test first
  • Write enough code to make the test pass
  • Refactor
  • Source Control
  • Key Concepts
  • Ruby
  • Testing
  • Software Engineering
Edit on GitHub
  1. Fundamentals

Loops, Blocks & Strings

When we want to do something repeatedly, we use loops. There are many ways to create loops in Ruby, in this chapter we're going to see a couple of them.

Text Banner

We're going to write a method that adds a decoration to a string to make it look like it's in a banner, like this:

================
this is a banner
================

Starting the banner project:

# define TDD_RUBY_PATH in your shell configuration
cd $TDD_RUBY_PATH
mkdir string-decorator
cd string-decorator

Then let's start with the test.

Write the test first

Here's our first test in a file called string_decorator_test.rb:

require "minitest/autorun"
require_relative "string_decorator"

class TestStringDecorator < Minitest::Test
  def test_banner
    expected = "====\ntext\n====\n"
    assert_equal expected, StringDecorator.new.banner("text")
  end
end

Maybe you felt a little uncomfortable with that hard-to-read value assigned to expected. Yeah, me too.

I think that mentally counting the amount of = symbols to put above and below the text is very error prone. It can make future maintainer's life worse. Just to think that the future maintainer can be you, it should trigger a desire to find a better way to write that.

Here Documents

class TestStringDecorator < Minitest::Test
  def test_banner
    expected = <<~BANNER
      ====
      text
      ====
    BANNER
    assert_equal expected, StringDecorator.new.banner("text")
  end
end

Here we're using a "squiggly heredoc" so we can have indented content between the opening identifier (<<~BANNER) and the closing one (BANNER)

Write the minimal amount of code for the test to run

Running the test:

string_decorator_test.rb:2:in `require_relative': cannot load such file -- /path/to/string_decorator (LoadError)
        from string_decorator_test.rb:2:in `<main>'

As expected, we got an error (not a failure).

Keep the discipline! You don't need to know anything new to make the test fail properly.

All you need to do right now is write enough code to make the test fail with no errors.

If you let the error messages drive the development you'll end up with a file named string_decorator.rb with this content:

class StringDecorator
  def banner(text)
  end
end

Run the test and now it should fail with no errors.

Write enough code to make the test pass

In order to create the banner we need:

  1. check how many characters there are in the given string

  2. print, in a line, the = character the same amount of times as the length of the given string

  3. print the given string in a new line

  4. print again the same amount of = characters in a new line

String length

Let's experiment in irb:

# IRB SESSION

> 'foo'.length
#=> 3

> 'meleu'.length
#=> 5

> 'long string with many words...'.length
#=> 30

Great, that's exactly what we need!

Moving on...

Loops and Blocks

5.times do
  puts "Learn Ruby with TDD"
end

Run this code in irb and you'll see that it prints Learn Ruby with TDD 5 times.

Although that code is easy to read, almost like natural language, the way it's written can be new for us, programmers.

In that code we're using the #times method and also a block.

Blocks are frequently used in Ruby. It's like a way of bundling up a set of instructions for use elsewhere.

In our code above, the block starts with the keyword do and ends with the end. The block is being passed to the #times method to be executed.

We're going to talk more about blocks, but for now that's enough.

Concatenating Strings

The simplistic way to concatenate strings is by using the + plus sign, like this:

# IRB SESSION

> name = 'meleu'
#=> "meleu"

> 'Hello, ' + name
#=> "Hello, meleu"

If we want to append content to a variable we could use the += operator, like this:

# IRB SESSION

> msg = 'Hello'
#=> "Hello"

> msg += ', meleu'
#=> "Hello, meleu"

> msg
#=> "Hello, meleu"
# IRB SESSION

> msg = 'Hello'
#=> "Hello"

> msg << ', meleu'
#=> "Hello, meleu"

> msg
#=> "Hello, meleu"

The final result can look like the same, but there are subtle differences between += and <<. The most notable one is that using << has a better performance, specially when used in a loop, which is our case. So, let's stick with the << operator.

First implementation

Let's recap what we've just learned:

  1. Get the string length with text.length

  2. Repeat instructions in a loop x times by

    • bundling up a set of instructions in a block

    • passing the block to x.times

  3. Append contents to a string variable with var << 'more content'

Combining these techniques we can create a text banner function like this:

class StringDecorator
  def banner(text)
    # create an empty string
    border = ""

    # get the text length and call the
    # .times method to create a loop
    text.length.times do
      border << "=" # appending '=' to the border
    end

    # interpolating the border before and after the text
    "#{border}\n#{text}\n#{border}\n"
  end
end

I've put some comments just to explain what we're doing now. In the refactor phase we're going to clean them up.

Maybe the only thing that require an extra explanation is the text.length.times expression. We can use that because String#length returns an Integer, and then we can use Integer#times, which is what creates the loop

Let's run the test:

Run options: --seed 15260

# Running:

.

Finished in 0.000478s, 2092.0506 runs/s, 2092.0506 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Great! A successful test triggers our brain to make that decision: refactor or commit?

Let's refactor!

Refactor

I'd like to use this refactoring session to talk about blocks again...

Bracket Blocks

When a block contains just a single instruction, it's usual to use { brackets } to delimiter the block, like this:

5.times { puts "Learn Ruby with TDD" }

Run this 👆 in irb and confirm.

With this new knowledge we can make our code shorter:

class StringDecorator
  def banner(text)
    border = ""
    text.length.times { border << "=" }
    "#{border}\n#{text}\n#{border}\n"
  end
end

Run the test and you'll see that it's still working fine.

String multiplication

"Ho! " * 3
# => "Ho! Ho! Ho! "

This is an example of how expressive the Ruby language can be.

With this knowledge we can refactor our banner function so it doesn't even need a loop:

class StringDecorator
  def banner(text)
    border = "=" * text.length
    "#{border}\n#{text}\n#{border}\n"
  end
end

Run the test to confirm this code is working as expected, and then we're done with this feature.

Source Control

git add banner_test.rb banner.rb
git commit -m 'feat(banner): put text in a banner'

A bug report arrives

Let's imagine we released our software to the world. This is when the "real work" begins: maintaining our software. We start to see our code being used in ways we didn't anticipate, and bugs start to appear.

A few days after publishing our code a user submitted a bug report saying that it doesn't work with multiline strings.

When we receive a bug report the very first thing to do is to go reproduce it. Let's do it in irb (be sure to be in the same directory as your code):

# IRB SESSION #

> # yeah! we can require code in irb too!
> require_relative 'string_decorator'
#=> true

> text = "this is\na multiline\nstring"
#=> "this is\na multiline\nstring"

> # confirming it's multiline
> puts text
this is
a multiline
string
#=> nil

> puts StringDecorator.new.banner(text)
==========================
this is
a multiline
string
==========================
#=> nil

Indeed, that's not what we would expect from the banner code. We didn't envisaged this use case when we were developing. Such situation is one of the most common things in the day-to-day work of a professional developer, we must get used to it.

A system must be continually adapted or it becomes progressively less satisfactory.

Once we confirmed that the bug report is valid. Then let's fix it reproduce it in a test case.

Write the test first

We expect the borders to be as long as the longest line, then let's write a test for it:

# ...
class TestStringDecorator < Minitest::Test
  # ...
  
  def test_banner_with_multiple_lines
    expected = <<~BANNER
      =============
      text
      with multiple
      lines
      =============
    BANNER
    text = "text\nwith multiple\nlines"
    assert_equal expected, StringDecorator.new.banner(text)
  end
end

Run the test to see the error/failure message:

  1) Failure:
TestStringDecorator#test_banner_with_multiple_lines [string_decorator_test.rb:23]:
--- expected
+++ actual
@@ -1,6 +1,6 @@
-"=============
+"========================
 text
 with multiple
 lines
-=============
+========================
 "

Good thing that the test failed with no errors, but we still need to sort the failure.

The output shows exactly the difference between the expected and the actual borders, while the text in between remains the same.

Now that we have a good test, we can work on our fix.

Write enough code to make the test pass

If our border needs to be as long as the longest line in the string, we need a way to check each line length.

Iterating over each line of a String

When we call String#each_line giving a block to it, it creates substrings that are the result of splitting the original string at each occurrence of a new line. The new thing for us here is that #each_line needs a place (a variable) to where it puts the created substring. In this case we need to use a block with a parameter.

Block Parameters

When the instructions within our block need to reference the value they're currently working with, we specify a block parameter. Let's see an example in irb:

# IRB SESSION

> "multi\nline\nstring".each_line { |line| p line }
"multi\n"
"line\n"
"string"
#=> "multi\nline\nstring"

That looks promising! 🙂

Longest line

Let's use what we've learned to check the longest line. Going back to our string_decorator.rb:

def banner(text)
  # assume the max_length is zero
  max_length = 0

  # loop over each line and check if its length > max_length
  text.each_line do |line|
    length = line.length
    max_length = length if length > max_length
  end
  # create the border
  border = '=' * max_length

  "#{border}\n#{text}\n#{border}\n"
end

In the loop above we're checking if the current line length is longer than max_length. If that's true, then we update the value of max_length.

Running the test:

  1) Failure:
TestBanner#test_banner_with_multiple_lines [banner_test.rb:25]:
--- expected
+++ actual
@@ -1,6 +1,6 @@
-"=============
+"==============
 text
 with multiple
 lines
-=============
+==============
 "

Whoops! Looks like there's an extra = character in the border.

The reason for this is that String#each_line creates substrings by splitting the original string at the occurrences of a new line, but it preserves the newline character.

We could see that in our irb session:

# IRB SESSION #

> "multi\nline\nstring".each_line { |line| p line }
"multi\n"
"line\n"
"string"
#=> "multi\nline\nstring"
# IRB SESSION #

> "hello\n".chomp
#=> "hello"

Great, that's what we want.

Here's the new version of our code using #chomp:

def banner(text)
  max_length = 0
  text.each_line do |line|
    # using chomp 👇 here
    length = line.chomp.length
    max_length = length if length > max_length
  end
  border = '=' * max_length

  "#{border}\n#{text}\n#{border}\n"
end

Run the tests and they should pass now.

Time for refactoring.

Refactor

I think our banner code is starting to accumulate too much logic in it. It should be a function that just puts borders above and below a given text.

As a system evolves, its complexity increases unless work is done to maintain or reduce it.

We need to proactively fight against this increasing complexity. In our case here we're going to achieve this by delegating the longest-line-detection logic to a separate function

class StringDecorator
  def banner(text)
    border = "=" * max_line_length(text)
    "#{border}\n#{text}\n#{border}\n"
  end

  private

  def max_line_length(text)
    max = 0
    text.each_line do |line|
      line_length = line.chomp.length
      max = line_length if line_length > max
    end
    max
  end
end

Run the tests and they should pass.

For now I'm happy with this version. So, let's move on.

Source Control

git add banner_test.rb banner.rb
git commit -m 'fix(banner): handle multiline text'

Key Concepts

We learned a good amount of things for such a simple functionality, didn't we? Let's recap!

Ruby

  • Heredocs

  • Loops with Integer#times

  • Blocks

    • with do/end

    • with { brackets }

    • with a parameter

  • String methods

    • concatenating with << (aka shovel operator)

    • string "multiplication" with *

    • length

    • each_line

    • chomp

Testing

  • Reinforced TDD practices (write test first!)

  • After receiving a bug report:

    • first reproduce it in a test case

    • then start working on the fix

Software Engineering

Continuing Change: A system must be continually adapted or it becomes progressively less satisfactory.

Increasing Complexity: As a system evolves, its complexity increases unless work is done to maintain or reduce it.

PreviousObjects, Methods & IntegersNextArrays & Floats

Last updated 1 month ago

A (aka heredoc) is a more comfortable way to read a multilinear block of text. In our case, as we want to check if the borders of the banner have the proper length in a quick glance, we would use a heredoc like this:

Check the if you need more info about heredocs.

For our first goal, we can check the String class documentation and look if it has a method to give us the length of a string. Turns out that such method is the .

One of the simplest ways to create a loop in Ruby is by using . Here's an example:

Although it's possible to append contents to a string variable with the += operator, the most usual Rubyist way to do it is by using the : <<

has some good answers about the differences between += and <<. Including some code where you can prove the difference in performance.

One interesting thing in is that we can use string * integer to get a new string containing integer copies of the original string. Like this:

In fact this situation is so common that more than fifty years ago a computer scientist named already noticed that and said:

This is the first of the eight

In the String class page we can see .

In each iteration the substring is stored in the line variable and we can use it however we want. In the example above we're just inspecting it with .

So, in order to accurately get the length of a line we must ignore the trailing newline character. Fortunately we have method for that: . Let's check in irb:

Unfortunately real world is not that simple. We found a need to handle the case where the input text has multiple lines. Our banner is handling this but its complexity increased. This situation reminds another one of the :

The two first .

here document
official documentation
String#length
the Integer#times method
shovel operator
This StackOverflow Q&A
String documentation
Manny Lehman
Lehman's laws of software evolution
a method called #each_line
the Kernel#p method
String#chomp
Lehman's laws
Lehman's laws of software evolution